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