Websocket.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449
  1. <template>
  2. <Splitpanes
  3. class="smart-splitter"
  4. :rtl="SIDEBAR_ON_LEFT && windowInnerWidth.x.value >= 768"
  5. :class="{
  6. '!flex-row-reverse': SIDEBAR_ON_LEFT && windowInnerWidth.x.value >= 768,
  7. }"
  8. :horizontal="!(windowInnerWidth.x.value >= 768)"
  9. >
  10. <Pane size="75" min-size="65" class="hide-scrollbar !overflow-auto">
  11. <Splitpanes class="smart-splitter" :horizontal="COLUMN_LAYOUT">
  12. <Pane class="hide-scrollbar !overflow-auto">
  13. <AppSection label="request">
  14. <div class="bg-primary flex p-4 top-0 z-10 sticky">
  15. <div class="space-x-2 flex-1 inline-flex">
  16. <input
  17. id="websocket-url"
  18. v-model="url"
  19. class="
  20. bg-primaryLight
  21. border border-divider
  22. rounded
  23. text-secondaryDark
  24. w-full
  25. py-2
  26. px-4
  27. hover:border-dividerDark
  28. focus-visible:bg-transparent
  29. focus-visible:border-dividerDark
  30. "
  31. type="url"
  32. autocomplete="off"
  33. spellcheck="false"
  34. :class="{ error: !urlValid }"
  35. :placeholder="$t('websocket.url')"
  36. :disabled="connectionState"
  37. @keyup.enter="urlValid ? toggleConnection() : null"
  38. />
  39. <ButtonPrimary
  40. id="connect"
  41. :disabled="!urlValid"
  42. class="w-32"
  43. name="connect"
  44. :label="
  45. !connectionState
  46. ? $t('action.connect')
  47. : $t('action.disconnect')
  48. "
  49. :loading="connectingState"
  50. @click.native="toggleConnection"
  51. />
  52. </div>
  53. </div>
  54. <div
  55. class="
  56. bg-primary
  57. border-b border-dividerLight
  58. flex flex-1
  59. top-upperPrimaryStickyFold
  60. pl-4
  61. z-10
  62. sticky
  63. items-center
  64. justify-between
  65. "
  66. >
  67. <label class="font-semibold text-secondaryLight">
  68. {{ $t("websocket.protocols") }}
  69. </label>
  70. <div class="flex">
  71. <ButtonSecondary
  72. v-tippy="{ theme: 'tooltip' }"
  73. :title="$t('action.clear_all')"
  74. svg="trash-2"
  75. @click.native="clearContent"
  76. />
  77. <ButtonSecondary
  78. v-tippy="{ theme: 'tooltip' }"
  79. :title="$t('add.new')"
  80. svg="plus"
  81. @click.native="addProtocol"
  82. />
  83. </div>
  84. </div>
  85. <div
  86. v-for="(protocol, index) of protocols"
  87. :key="`protocol-${index}`"
  88. class="
  89. divide-x divide-dividerLight
  90. border-b border-dividerLight
  91. flex
  92. "
  93. >
  94. <input
  95. v-model="protocol.value"
  96. class="bg-transparent flex flex-1 py-2 px-4"
  97. :placeholder="$t('count.protocol', { count: index + 1 })"
  98. name="message"
  99. type="text"
  100. autocomplete="off"
  101. />
  102. <span>
  103. <ButtonSecondary
  104. v-tippy="{ theme: 'tooltip' }"
  105. :title="
  106. protocol.hasOwnProperty('active')
  107. ? protocol.active
  108. ? $t('action.turn_off')
  109. : $t('action.turn_on')
  110. : $t('action.turn_off')
  111. "
  112. :svg="
  113. protocol.hasOwnProperty('active')
  114. ? protocol.active
  115. ? 'check-circle'
  116. : 'circle'
  117. : 'check-circle'
  118. "
  119. color="green"
  120. @click.native="
  121. protocol.active = protocol.hasOwnProperty('active')
  122. ? !protocol.active
  123. : false
  124. "
  125. />
  126. </span>
  127. <span>
  128. <ButtonSecondary
  129. v-tippy="{ theme: 'tooltip' }"
  130. :title="$t('action.remove')"
  131. svg="trash"
  132. color="red"
  133. @click.native="deleteProtocol({ index })"
  134. />
  135. </span>
  136. </div>
  137. <div
  138. v-if="protocols.length === 0"
  139. class="
  140. flex flex-col
  141. text-secondaryLight
  142. p-4
  143. items-center
  144. justify-center
  145. "
  146. >
  147. <img
  148. :src="`/images/states/${$colorMode.value}/add_category.svg`"
  149. loading="lazy"
  150. class="
  151. flex-col
  152. my-4
  153. object-contain object-center
  154. h-16
  155. w-16
  156. inline-flex
  157. "
  158. :alt="$t('empty.protocols')"
  159. />
  160. <span class="text-center mb-4">
  161. {{ $t("empty.protocols") }}
  162. </span>
  163. </div>
  164. </AppSection>
  165. </Pane>
  166. <Pane class="hide-scrollbar !overflow-auto">
  167. <AppSection label="response">
  168. <RealtimeLog
  169. :title="$t('websocket.log')"
  170. :log="communication.log"
  171. />
  172. </AppSection>
  173. </Pane>
  174. </Splitpanes>
  175. </Pane>
  176. <Pane
  177. v-if="SIDEBAR"
  178. size="25"
  179. min-size="20"
  180. class="hide-scrollbar !overflow-auto"
  181. >
  182. <AppSection label="messages">
  183. <div class="flex flex-col flex-1 p-4 inline-flex">
  184. <label
  185. for="websocket-message"
  186. class="font-semibold text-secondaryLight"
  187. >
  188. {{ $t("websocket.communication") }}
  189. </label>
  190. </div>
  191. <div class="flex space-x-2 px-4">
  192. <input
  193. id="websocket-message"
  194. v-model="communication.input"
  195. name="message"
  196. type="text"
  197. autocomplete="off"
  198. :disabled="!connectionState"
  199. :placeholder="$t('websocket.message')"
  200. class="input"
  201. @keyup.enter="connectionState ? sendMessage() : null"
  202. @keyup.up="connectionState ? walkHistory('up') : null"
  203. @keyup.down="connectionState ? walkHistory('down') : null"
  204. />
  205. <ButtonPrimary
  206. id="send"
  207. name="send"
  208. :disabled="!connectionState"
  209. :label="$t('action.send')"
  210. @click.native="sendMessage"
  211. />
  212. </div>
  213. </AppSection>
  214. </Pane>
  215. </Splitpanes>
  216. </template>
  217. <script>
  218. import { defineComponent } from "@nuxtjs/composition-api"
  219. import { Splitpanes, Pane } from "splitpanes"
  220. import "splitpanes/dist/splitpanes.css"
  221. import debounce from "lodash/debounce"
  222. import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
  223. import useWindowSize from "~/helpers/utils/useWindowSize"
  224. import { useSetting } from "~/newstore/settings"
  225. export default defineComponent({
  226. components: { Splitpanes, Pane },
  227. setup() {
  228. return {
  229. windowInnerWidth: useWindowSize(),
  230. SIDEBAR: useSetting("SIDEBAR"),
  231. COLUMN_LAYOUT: useSetting("COLUMN_LAYOUT"),
  232. SIDEBAR_ON_LEFT: useSetting("SIDEBAR_ON_LEFT"),
  233. }
  234. },
  235. data() {
  236. return {
  237. connectionState: false,
  238. connectingState: false,
  239. url: "wss://hoppscotch-websocket.herokuapp.com",
  240. isUrlValid: true,
  241. socket: null,
  242. communication: {
  243. log: null,
  244. input: "",
  245. },
  246. currentIndex: -1, // index of the message log array to put in input box
  247. protocols: [],
  248. activeProtocols: [],
  249. }
  250. },
  251. computed: {
  252. urlValid() {
  253. return this.isUrlValid
  254. },
  255. },
  256. watch: {
  257. url() {
  258. this.debouncer()
  259. },
  260. protocols: {
  261. handler(newVal) {
  262. this.activeProtocols = newVal
  263. .filter((item) =>
  264. Object.prototype.hasOwnProperty.call(item, "active")
  265. ? item.active === true
  266. : true
  267. )
  268. .map(({ value }) => value)
  269. },
  270. deep: true,
  271. },
  272. },
  273. created() {
  274. if (process.browser) {
  275. this.worker = this.$worker.createRejexWorker()
  276. this.worker.addEventListener("message", this.workerResponseHandler)
  277. }
  278. },
  279. destroyed() {
  280. this.worker.terminate()
  281. },
  282. methods: {
  283. clearContent() {
  284. this.protocols = []
  285. },
  286. debouncer: debounce(function () {
  287. this.worker.postMessage({ type: "ws", url: this.url })
  288. }, 1000),
  289. workerResponseHandler({ data }) {
  290. if (data.url === this.url) this.isUrlValid = data.result
  291. },
  292. toggleConnection() {
  293. // If it is connecting:
  294. if (!this.connectionState) return this.connect()
  295. // Otherwise, it's disconnecting.
  296. else return this.disconnect()
  297. },
  298. connect() {
  299. this.communication.log = [
  300. {
  301. payload: this.$t("state.connecting_to", { name: this.url }),
  302. source: "info",
  303. color: "var(--accent-color)",
  304. },
  305. ]
  306. try {
  307. this.connectingState = true
  308. this.socket = new WebSocket(this.url, this.activeProtocols)
  309. this.socket.onopen = () => {
  310. this.connectingState = false
  311. this.connectionState = true
  312. this.communication.log = [
  313. {
  314. payload: this.$t("state.connected_to", { name: this.url }),
  315. source: "info",
  316. color: "var(--accent-color)",
  317. ts: new Date().toLocaleTimeString(),
  318. },
  319. ]
  320. this.$toast.success(this.$t("state.connected"), {
  321. icon: "sync",
  322. })
  323. }
  324. this.socket.onerror = () => {
  325. this.handleError()
  326. }
  327. this.socket.onclose = () => {
  328. this.connectionState = false
  329. this.communication.log.push({
  330. payload: this.$t("state.disconnected_from", { name: this.url }),
  331. source: "info",
  332. color: "#ff5555",
  333. ts: new Date().toLocaleTimeString(),
  334. })
  335. this.$toast.error(this.$t("state.disconnected"), {
  336. icon: "sync_disabled",
  337. })
  338. }
  339. this.socket.onmessage = ({ data }) => {
  340. this.communication.log.push({
  341. payload: data,
  342. source: "server",
  343. ts: new Date().toLocaleTimeString(),
  344. })
  345. }
  346. } catch (e) {
  347. this.handleError(e)
  348. this.$toast.error(this.$t("error.something_went_wrong"), {
  349. icon: "error_outline",
  350. })
  351. }
  352. logHoppRequestRunToAnalytics({
  353. platform: "wss",
  354. })
  355. },
  356. disconnect() {
  357. if (this.socket) {
  358. this.socket.close()
  359. this.connectionState = false
  360. this.connectingState = false
  361. }
  362. },
  363. handleError(error) {
  364. this.disconnect()
  365. this.connectionState = false
  366. this.communication.log.push({
  367. payload: this.$t("error.something_went_wrong"),
  368. source: "info",
  369. color: "#ff5555",
  370. ts: new Date().toLocaleTimeString(),
  371. })
  372. if (error !== null)
  373. this.communication.log.push({
  374. payload: error,
  375. source: "info",
  376. color: "#ff5555",
  377. ts: new Date().toLocaleTimeString(),
  378. })
  379. },
  380. sendMessage() {
  381. const message = this.communication.input
  382. this.socket.send(message)
  383. this.communication.log.push({
  384. payload: message,
  385. source: "client",
  386. ts: new Date().toLocaleTimeString(),
  387. })
  388. this.communication.input = ""
  389. },
  390. walkHistory(direction) {
  391. const clientMessages = this.communication.log.filter(
  392. ({ source }) => source === "client"
  393. )
  394. const length = clientMessages.length
  395. switch (direction) {
  396. case "up":
  397. if (length > 0 && this.currentIndex !== 0) {
  398. // does nothing if message log is empty or the currentIndex is 0 when up arrow is pressed
  399. if (this.currentIndex === -1) {
  400. this.currentIndex = length - 1
  401. this.communication.input =
  402. clientMessages[this.currentIndex].payload
  403. } else if (this.currentIndex === 0) {
  404. this.communication.input = clientMessages[0].payload
  405. } else if (this.currentIndex > 0) {
  406. this.currentIndex = this.currentIndex - 1
  407. this.communication.input =
  408. clientMessages[this.currentIndex].payload
  409. }
  410. }
  411. break
  412. case "down":
  413. if (length > 0 && this.currentIndex > -1) {
  414. if (this.currentIndex === length - 1) {
  415. this.currentIndex = -1
  416. this.communication.input = ""
  417. } else if (this.currentIndex < length - 1) {
  418. this.currentIndex = this.currentIndex + 1
  419. this.communication.input =
  420. clientMessages[this.currentIndex].payload
  421. }
  422. }
  423. break
  424. }
  425. },
  426. addProtocol() {
  427. this.protocols.push({ value: "", active: true })
  428. },
  429. deleteProtocol({ index }) {
  430. const oldProtocols = this.protocols.slice()
  431. this.$delete(this.protocols, index)
  432. this.$toast.success(this.$t("state.deleted"), {
  433. icon: "delete",
  434. action: {
  435. text: this.$t("action.undo"),
  436. duration: 4000,
  437. onClick: (_, toastObject) => {
  438. this.protocols = oldProtocols
  439. toastObject.remove()
  440. },
  441. },
  442. })
  443. },
  444. },
  445. })
  446. </script>