Websocket.vue 13 KB

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