MQTT Revamp (#2381)
Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
This commit is contained in:
@@ -161,6 +161,7 @@
|
|||||||
"body": "This request does not have a body",
|
"body": "This request does not have a body",
|
||||||
"collection": "Collection is empty",
|
"collection": "Collection is empty",
|
||||||
"collections": "Collections are empty",
|
"collections": "Collections are empty",
|
||||||
|
"subscription": "Subscriptions are empty",
|
||||||
"documentation": "Connect to a GraphQL endpoint to view documentation",
|
"documentation": "Connect to a GraphQL endpoint to view documentation",
|
||||||
"endpoint": "Endpoint cannot be empty",
|
"endpoint": "Endpoint cannot be empty",
|
||||||
"environments": "Environments are empty",
|
"environments": "Environments are empty",
|
||||||
@@ -312,6 +313,8 @@
|
|||||||
"import_export": "Import / Export"
|
"import_export": "Import / Export"
|
||||||
},
|
},
|
||||||
"mqtt": {
|
"mqtt": {
|
||||||
|
"new": "New Subscription",
|
||||||
|
"invalid_topic": "Please provide a topic for the subscription",
|
||||||
"communication": "Communication",
|
"communication": "Communication",
|
||||||
"log": "Log",
|
"log": "Log",
|
||||||
"message": "Message",
|
"message": "Message",
|
||||||
@@ -321,7 +324,23 @@
|
|||||||
"topic_name": "Topic Name",
|
"topic_name": "Topic Name",
|
||||||
"topic_title": "Publish / Subscribe topic",
|
"topic_title": "Publish / Subscribe topic",
|
||||||
"unsubscribe": "Unsubscribe",
|
"unsubscribe": "Unsubscribe",
|
||||||
"url": "URL"
|
"url": "URL",
|
||||||
|
"client_id": "Client ID",
|
||||||
|
"qos": "QoS",
|
||||||
|
"color": "Color",
|
||||||
|
"connection_not_authorized": "This MQTT connection does not use any authentication.",
|
||||||
|
"not_connected": "Please start a MQTT connection first.",
|
||||||
|
"already_subscribed": "You are already subscribed to this topic.",
|
||||||
|
"connection_config": "Connection Config",
|
||||||
|
"keep_alive": "Keep Alive",
|
||||||
|
"clean_session": "Clean Session",
|
||||||
|
"ssl": "SSL",
|
||||||
|
"lw_qos": "Last-Will QoS",
|
||||||
|
"lw_retain": "Last-Will Retain",
|
||||||
|
"lw_topic": "Last-Will Topic",
|
||||||
|
"lw_message": "Last-Will Message",
|
||||||
|
"clear_input": "Clear input",
|
||||||
|
"clear_input_on_send": "Clear input on send"
|
||||||
},
|
},
|
||||||
"navigation": {
|
"navigation": {
|
||||||
"doc": "Docs",
|
"doc": "Docs",
|
||||||
@@ -530,6 +549,15 @@
|
|||||||
"title": "Theme"
|
"title": "Theme"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"shortcodes": {
|
||||||
|
"actions": "Actions",
|
||||||
|
"created_on": "Created on",
|
||||||
|
"deleted": "Shortcode deleted",
|
||||||
|
"method": "Method",
|
||||||
|
"not_found": "Shortcode not found",
|
||||||
|
"short_code": "Short code",
|
||||||
|
"url": "URL"
|
||||||
|
},
|
||||||
"show": {
|
"show": {
|
||||||
"code": "Show code",
|
"code": "Show code",
|
||||||
"collection": "Expand Collection Panel",
|
"collection": "Expand Collection Panel",
|
||||||
@@ -539,7 +567,7 @@
|
|||||||
"socketio": {
|
"socketio": {
|
||||||
"communication": "Communication",
|
"communication": "Communication",
|
||||||
"connection_not_authorized": "This SocketIO connection does not use any authentication.",
|
"connection_not_authorized": "This SocketIO connection does not use any authentication.",
|
||||||
"event_name": "Event Name",
|
"event_name": "Event/Topic Name",
|
||||||
"events": "Events",
|
"events": "Events",
|
||||||
"log": "Log",
|
"log": "Log",
|
||||||
"url": "URL"
|
"url": "URL"
|
||||||
|
|||||||
5
packages/hoppscotch-app/src/components.d.ts
vendored
5
packages/hoppscotch-app/src/components.d.ts
vendored
@@ -106,6 +106,7 @@ declare module '@vue/runtime-core' {
|
|||||||
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
|
IconLucideLayers: typeof import('~icons/lucide/layers')['default']
|
||||||
IconLucideLoader: typeof import('~icons/lucide/loader')['default']
|
IconLucideLoader: typeof import('~icons/lucide/loader')['default']
|
||||||
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
|
IconLucideMinus: typeof import('~icons/lucide/minus')['default']
|
||||||
|
IconLucideRss: typeof import('~icons/lucide/rss')['default']
|
||||||
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
IconLucideSearch: typeof import('~icons/lucide/search')['default']
|
||||||
IconLucideUser: typeof import('~icons/lucide/user')['default']
|
IconLucideUser: typeof import('~icons/lucide/user')['default']
|
||||||
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
IconLucideUsers: typeof import('~icons/lucide/users')['default']
|
||||||
@@ -121,8 +122,10 @@ declare module '@vue/runtime-core' {
|
|||||||
ProfilePicture: typeof import('./components/profile/Picture.vue')['default']
|
ProfilePicture: typeof import('./components/profile/Picture.vue')['default']
|
||||||
ProfileShortcode: typeof import('./components/profile/Shortcode.vue')['default']
|
ProfileShortcode: typeof import('./components/profile/Shortcode.vue')['default']
|
||||||
RealtimeCommunication: typeof import('./components/realtime/Communication.vue')['default']
|
RealtimeCommunication: typeof import('./components/realtime/Communication.vue')['default']
|
||||||
|
RealtimeConnectionConfig: typeof import('./components/realtime/ConnectionConfig.vue')['default']
|
||||||
RealtimeLog: typeof import('./components/realtime/Log.vue')['default']
|
RealtimeLog: typeof import('./components/realtime/Log.vue')['default']
|
||||||
RealtimeLogEntry: typeof import('./components/realtime/LogEntry.vue')['default']
|
RealtimeLogEntry: typeof import('./components/realtime/LogEntry.vue')['default']
|
||||||
|
RealtimeSubscription: typeof import('./components/realtime/Subscription.vue')['default']
|
||||||
SmartAccentModePicker: typeof import('./components/smart/AccentModePicker.vue')['default']
|
SmartAccentModePicker: typeof import('./components/smart/AccentModePicker.vue')['default']
|
||||||
SmartAnchor: typeof import('./components/smart/Anchor.vue')['default']
|
SmartAnchor: typeof import('./components/smart/Anchor.vue')['default']
|
||||||
SmartAutoComplete: typeof import('./components/smart/AutoComplete.vue')['default']
|
SmartAutoComplete: typeof import('./components/smart/AutoComplete.vue')['default']
|
||||||
@@ -146,6 +149,8 @@ declare module '@vue/runtime-core' {
|
|||||||
SmartTab: typeof import('./components/smart/Tab.vue')['default']
|
SmartTab: typeof import('./components/smart/Tab.vue')['default']
|
||||||
SmartTabs: typeof import('./components/smart/Tabs.vue')['default']
|
SmartTabs: typeof import('./components/smart/Tabs.vue')['default']
|
||||||
SmartToggle: typeof import('./components/smart/Toggle.vue')['default']
|
SmartToggle: typeof import('./components/smart/Toggle.vue')['default']
|
||||||
|
SmartWindow: typeof import('./components/smart/Window.vue')['default']
|
||||||
|
SmartWindows: typeof import('./components/smart/Windows.vue')['default']
|
||||||
TabPrimary: typeof import('./components/tab/Primary.vue')['default']
|
TabPrimary: typeof import('./components/tab/Primary.vue')['default']
|
||||||
TabSecondary: typeof import('./components/tab/Secondary.vue')['default']
|
TabSecondary: typeof import('./components/tab/Secondary.vue')['default']
|
||||||
Teams: typeof import('./components/teams/index.vue')['default']
|
Teams: typeof import('./components/teams/index.vue')['default']
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
v-tippy="{ theme: 'tooltip', delay: [500, 20], allowHTML: true }"
|
v-tippy="{ theme: 'tooltip', delay: [500, 20], allowHTML: true }"
|
||||||
:title="`${t(
|
:title="`${t(
|
||||||
'request.run'
|
'request.run'
|
||||||
)} <kbd>${getSpecialKey()}</kbd><kbd>G</kbd>`"
|
)} <kbd>${getSpecialKey()}</kbd><kbd>↩</kbd>`"
|
||||||
:label="`${t('request.run')}`"
|
:label="`${t('request.run')}`"
|
||||||
:icon="IconPlay"
|
:icon="IconPlay"
|
||||||
class="rounded-none !text-accent !hover:text-accentDark"
|
class="rounded-none !text-accent !hover:text-accentDark"
|
||||||
|
|||||||
@@ -48,6 +48,12 @@
|
|||||||
>
|
>
|
||||||
<div class="flex pb-4 my-4 space-x-2">
|
<div class="flex pb-4 my-4 space-x-2">
|
||||||
<div class="flex flex-col items-end space-y-4 text-right">
|
<div class="flex flex-col items-end space-y-4 text-right">
|
||||||
|
<span class="flex items-center flex-1">
|
||||||
|
{{ t("shortcut.request.send_request") }}
|
||||||
|
</span>
|
||||||
|
<span class="flex items-center flex-1">
|
||||||
|
{{ t("shortcut.general.show_all") }}
|
||||||
|
</span>
|
||||||
<span class="flex items-center flex-1">
|
<span class="flex items-center flex-1">
|
||||||
{{ t("shortcut.general.command_menu") }}
|
{{ t("shortcut.general.command_menu") }}
|
||||||
</span>
|
</span>
|
||||||
@@ -56,6 +62,14 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col space-y-4">
|
<div class="flex flex-col space-y-4">
|
||||||
|
<div class="flex">
|
||||||
|
<kbd class="shortcut-key">{{ getSpecialKey() }}</kbd>
|
||||||
|
<kbd class="shortcut-key">↩</kbd>
|
||||||
|
</div>
|
||||||
|
<div class="flex">
|
||||||
|
<kbd class="shortcut-key">{{ getSpecialKey() }}</kbd>
|
||||||
|
<kbd class="shortcut-key">K</kbd>
|
||||||
|
</div>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<kbd class="shortcut-key">/</kbd>
|
<kbd class="shortcut-key">/</kbd>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<span class="select-wrapper">
|
<span class="select-wrapper">
|
||||||
<input
|
<input
|
||||||
id="method"
|
id="method"
|
||||||
class="flex px-4 py-2 font-semibold rounded-l cursor-pointer transition text-secondaryDark w-26 bg-primaryLight"
|
class="flex px-4 py-2 font-semibold transition rounded-l cursor-pointer text-secondaryDark w-26 bg-primaryLight"
|
||||||
:value="newMethod"
|
:value="newMethod"
|
||||||
:readonly="!isCustomMethod"
|
:readonly="!isCustomMethod"
|
||||||
:placeholder="`${t('request.method')}`"
|
:placeholder="`${t('request.method')}`"
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex flex-1 overflow-auto border-l rounded-r transition border-divider bg-primaryLight whitespace-nowrap"
|
class="flex flex-1 overflow-auto transition border-l rounded-r border-divider bg-primaryLight whitespace-nowrap"
|
||||||
>
|
>
|
||||||
<SmartEnvInput
|
<SmartEnvInput
|
||||||
v-model="newEndpoint"
|
v-model="newEndpoint"
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
<ButtonPrimary
|
<ButtonPrimary
|
||||||
id="send"
|
id="send"
|
||||||
v-tippy="{ theme: 'tooltip', delay: [500, 20], allowHTML: true }"
|
v-tippy="{ theme: 'tooltip', delay: [500, 20], allowHTML: true }"
|
||||||
:title="`${t('action.send')} <kbd>${getSpecialKey()}</kbd><kbd>G</kbd>`"
|
:title="`${t('action.send')} <kbd>${getSpecialKey()}</kbd><kbd>↩</kbd>`"
|
||||||
:label="`${!loading ? t('action.send') : t('action.cancel')}`"
|
:label="`${!loading ? t('action.send') : t('action.cancel')}`"
|
||||||
class="flex-1 rounded-r-none min-w-20"
|
class="flex-1 rounded-r-none min-w-20"
|
||||||
@click="!loading ? newSendRequest() : cancelRequest()"
|
@click="!loading ? newSendRequest() : cancelRequest()"
|
||||||
@@ -131,7 +131,7 @@
|
|||||||
</tippy>
|
</tippy>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="flex ml-2 border rounded transition border-dividerLight hover:border-dividerDark"
|
class="flex ml-2 transition border rounded border-dividerLight hover:border-dividerDark"
|
||||||
>
|
>
|
||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip', delay: [500, 20], allowHTML: true }"
|
v-tippy="{ theme: 'tooltip', delay: [500, 20], allowHTML: true }"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
class="flex flex-col items-center justify-center flex-1 text-secondaryLight"
|
class="flex flex-col items-center justify-center flex-1 text-secondaryLight"
|
||||||
>
|
>
|
||||||
<div class="flex pb-4 my-4 space-x-2">
|
<div class="flex pb-4 my-4 space-x-2">
|
||||||
<div class="flex flex-col items-end text-right space-y-4">
|
<div class="flex flex-col items-end space-y-4 text-right">
|
||||||
<span class="flex items-center flex-1">
|
<span class="flex items-center flex-1">
|
||||||
{{ t("shortcut.request.send_request") }}
|
{{ t("shortcut.request.send_request") }}
|
||||||
</span>
|
</span>
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
<div class="flex flex-col space-y-4">
|
<div class="flex flex-col space-y-4">
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<kbd class="shortcut-key">{{ getSpecialKey() }}</kbd>
|
<kbd class="shortcut-key">{{ getSpecialKey() }}</kbd>
|
||||||
<kbd class="shortcut-key">G</kbd>
|
<kbd class="shortcut-key">↩</kbd>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<kbd class="shortcut-key">{{ getSpecialKey() }}</kbd>
|
<kbd class="shortcut-key">{{ getSpecialKey() }}</kbd>
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col flex-1">
|
<div class="flex flex-col flex-1">
|
||||||
<div v-if="showEventField" class="flex items-center justify-between p-4">
|
<div
|
||||||
|
v-if="showEventField"
|
||||||
|
class="sticky z-10 flex items-center justify-center border-b bg-primary border-dividerLight"
|
||||||
|
:class="eventFieldStyles"
|
||||||
|
>
|
||||||
|
<icon-lucide-rss class="mx-4 svg-icons text-accentLight" />
|
||||||
<input
|
<input
|
||||||
id="event_name"
|
id="event_name"
|
||||||
v-model="eventName"
|
v-model="eventName"
|
||||||
class="input"
|
class="w-full py-2 pr-4 truncate bg-primary"
|
||||||
name="event_name"
|
name="event_name"
|
||||||
:placeholder="`${t('socketio.event_name')}`"
|
:placeholder="`${t('socketio.event_name')}`"
|
||||||
type="text"
|
type="text"
|
||||||
@@ -12,7 +17,8 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperSecondaryStickyFold"
|
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight"
|
||||||
|
:class="stickyHeaderStyles"
|
||||||
>
|
>
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
<label class="font-semibold text-secondaryLight">
|
<label class="font-semibold text-secondaryLight">
|
||||||
@@ -41,7 +47,9 @@
|
|||||||
v-for="(contentTypeItem, index) in validContentTypes"
|
v-for="(contentTypeItem, index) in validContentTypes"
|
||||||
:key="`contentTypeItem-${index}`"
|
:key="`contentTypeItem-${index}`"
|
||||||
:label="contentTypeItem"
|
:label="contentTypeItem"
|
||||||
:info-icon="contentTypeItem === contentType ? IconDone : null"
|
:info-icon="
|
||||||
|
contentTypeItem === contentType ? IconDone : undefined
|
||||||
|
"
|
||||||
:active-info-icon="contentTypeItem === contentType"
|
:active-info-icon="contentTypeItem === contentType"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
@@ -64,6 +72,15 @@
|
|||||||
class="rounded-none !text-accent !hover:text-accentDark"
|
class="rounded-none !text-accent !hover:text-accentDark"
|
||||||
@click="sendMessage()"
|
@click="sendMessage()"
|
||||||
/>
|
/>
|
||||||
|
<SmartCheckbox
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:on="clearInputOnSend"
|
||||||
|
class="px-2"
|
||||||
|
:title="`${t('mqtt.clear_input_on_send')}`"
|
||||||
|
@change="clearInputOnSend = !clearInputOnSend"
|
||||||
|
>
|
||||||
|
{{ t("mqtt.clear_input") }}
|
||||||
|
</SmartCheckbox>
|
||||||
<ButtonSecondary
|
<ButtonSecondary
|
||||||
v-tippy="{ theme: 'tooltip' }"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
to="https://docs.hoppscotch.io/realtime"
|
to="https://docs.hoppscotch.io/realtime"
|
||||||
@@ -132,12 +149,21 @@ import { readFileAsText } from "@functional/files"
|
|||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
import { useToast } from "@composables/toast"
|
import { useToast } from "@composables/toast"
|
||||||
import { isJSONContentType } from "@helpers/utils/contenttypes"
|
import { isJSONContentType } from "@helpers/utils/contenttypes"
|
||||||
|
import { defineActionHandler } from "~/helpers/actions"
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
showEventField: {
|
showEventField: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
eventFieldStyles: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
stickyHeaderStyles: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
isConnected: {
|
isConnected: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
@@ -164,6 +190,7 @@ const wsCommunicationBody = ref<HTMLElement>()
|
|||||||
const payload = ref<HTMLInputElement>()
|
const payload = ref<HTMLInputElement>()
|
||||||
|
|
||||||
const prettifyIcon = refAutoReset<Component>(IconWand2, 1000)
|
const prettifyIcon = refAutoReset<Component>(IconWand2, 1000)
|
||||||
|
const clearInputOnSend = ref(false)
|
||||||
|
|
||||||
const knownContentTypes = {
|
const knownContentTypes = {
|
||||||
JSON: "application/ld+json",
|
JSON: "application/ld+json",
|
||||||
@@ -197,7 +224,10 @@ useCodemirror(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const clearContent = () => {
|
const clearContent = () => {
|
||||||
|
if (clearInputOnSend.value) {
|
||||||
communicationBody.value = ""
|
communicationBody.value = ""
|
||||||
|
eventName.value = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendMessage = () => {
|
const sendMessage = () => {
|
||||||
@@ -207,10 +237,10 @@ const sendMessage = () => {
|
|||||||
eventName: eventName.value,
|
eventName: eventName.value,
|
||||||
message: communicationBody.value,
|
message: communicationBody.value,
|
||||||
})
|
})
|
||||||
communicationBody.value = ""
|
clearContent()
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadPayload = async (e: InputEvent) => {
|
const uploadPayload = async (e: Event) => {
|
||||||
const result = await pipe(
|
const result = await pipe(
|
||||||
(e.target as HTMLInputElement).files?.[0],
|
(e.target as HTMLInputElement).files?.[0],
|
||||||
TO.fromNullable,
|
TO.fromNullable,
|
||||||
@@ -235,4 +265,6 @@ const prettifyRequestBody = () => {
|
|||||||
toast.error(`${t("error.json_prettify_invalid_body")}`)
|
toast.error(`${t("error.json_prettify_invalid_body")}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defineActionHandler("request.send-cancel", sendMessage)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col flex-1">
|
||||||
|
<div
|
||||||
|
class="sticky z-10 flex items-center justify-between py-2 pl-4 pr-2 border-b bg-primary border-dividerLight top-upperPrimaryStickyFold"
|
||||||
|
>
|
||||||
|
<span class="flex items-center">
|
||||||
|
<label class="font-semibold text-secondaryLight">
|
||||||
|
{{ t("mqtt.connection_config") }}
|
||||||
|
</label>
|
||||||
|
</span>
|
||||||
|
<div class="flex">
|
||||||
|
<SmartCheckbox
|
||||||
|
:on="config.cleanSession"
|
||||||
|
class="px-2"
|
||||||
|
@change="config.cleanSession = !config.cleanSession"
|
||||||
|
>{{ t("mqtt.clean_session") }}
|
||||||
|
</SmartCheckbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-1 h-full border-dividerLight">
|
||||||
|
<div class="w-1/3 border-r border-dividerLight">
|
||||||
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
|
<SmartEnvInput
|
||||||
|
v-model="config.username"
|
||||||
|
:placeholder="t('authorization.username')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
|
<SmartEnvInput
|
||||||
|
v-model="config.password"
|
||||||
|
:placeholder="t('authorization.password')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center border-b border-dividerLight">
|
||||||
|
<label class="ml-4 text-secondaryLight">
|
||||||
|
{{ t("mqtt.keep_alive") }}
|
||||||
|
</label>
|
||||||
|
<SmartEnvInput
|
||||||
|
v-model="config.keepAlive"
|
||||||
|
:placeholder="t('mqtt.keep_alive')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-2/3">
|
||||||
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
|
<SmartEnvInput
|
||||||
|
v-model="config.lwTopic"
|
||||||
|
:placeholder="t('mqtt.lw_topic')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-1 border-b border-dividerLight">
|
||||||
|
<SmartEnvInput
|
||||||
|
v-model="config.lwMessage"
|
||||||
|
:placeholder="t('mqtt.lw_message')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-between px-4 border-b border-dividerLight"
|
||||||
|
>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<label class="font-semibold text-secondaryLight">
|
||||||
|
{{ t("mqtt.lw_qos") }}
|
||||||
|
</label>
|
||||||
|
<tippy interactive trigger="click" theme="popover">
|
||||||
|
<span class="select-wrapper">
|
||||||
|
<ButtonSecondary
|
||||||
|
class="pr-8 ml-2 rounded-none"
|
||||||
|
:label="`${config.lwQos}`"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<template #content="{ hide }">
|
||||||
|
<div class="flex flex-col" role="menu">
|
||||||
|
<SmartItem
|
||||||
|
v-for="item in QOS_VALUES"
|
||||||
|
:key="`qos-${item}`"
|
||||||
|
:label="`${item}`"
|
||||||
|
:icon="config.lwQos === item ? IconCheckCircle : IconCircle"
|
||||||
|
:active="config.lwQos === item"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
config.lwQos = item
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</tippy>
|
||||||
|
</div>
|
||||||
|
<SmartCheckbox
|
||||||
|
:on="config.lwRetain"
|
||||||
|
class="py-2"
|
||||||
|
@change="config.lwRetain = !config.lwRetain"
|
||||||
|
>{{ t("mqtt.lw_retain") }}
|
||||||
|
</SmartCheckbox>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import IconCheckCircle from "~icons/lucide/check-circle"
|
||||||
|
import IconCircle from "~icons/lucide/circle"
|
||||||
|
import { ref, watch } from "vue"
|
||||||
|
import { useI18n } from "@composables/i18n"
|
||||||
|
import {
|
||||||
|
MQTTConnectionConfig,
|
||||||
|
QOS_VALUES,
|
||||||
|
} from "~/helpers/realtime/MQTTConnection"
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "change", body: MQTTConnectionConfig): void
|
||||||
|
}>()
|
||||||
|
const config = ref<MQTTConnectionConfig>({
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
keepAlive: "60",
|
||||||
|
cleanSession: true,
|
||||||
|
lwTopic: "",
|
||||||
|
lwMessage: "",
|
||||||
|
lwQos: 0,
|
||||||
|
lwRetain: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
config,
|
||||||
|
(newVal) => {
|
||||||
|
emit("change", newVal)
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
</script>
|
||||||
@@ -52,6 +52,51 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
|
>
|
||||||
|
<div class="flex pb-4 my-4 space-x-2">
|
||||||
|
<div class="flex flex-col items-end space-y-4 text-right">
|
||||||
|
<span class="flex items-center flex-1">
|
||||||
|
{{ t("shortcut.request.send_request") }}
|
||||||
|
</span>
|
||||||
|
<span class="flex items-center flex-1">
|
||||||
|
{{ t("shortcut.general.show_all") }}
|
||||||
|
</span>
|
||||||
|
<span class="flex items-center flex-1">
|
||||||
|
{{ t("shortcut.general.command_menu") }}
|
||||||
|
</span>
|
||||||
|
<span class="flex items-center flex-1">
|
||||||
|
{{ t("shortcut.general.help_menu") }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col space-y-4">
|
||||||
|
<div class="flex">
|
||||||
|
<kbd class="shortcut-key">{{ getSpecialKey() }}</kbd>
|
||||||
|
<kbd class="shortcut-key">↩</kbd>
|
||||||
|
</div>
|
||||||
|
<div class="flex">
|
||||||
|
<kbd class="shortcut-key">{{ getSpecialKey() }}</kbd>
|
||||||
|
<kbd class="shortcut-key">K</kbd>
|
||||||
|
</div>
|
||||||
|
<div class="flex">
|
||||||
|
<kbd class="shortcut-key">/</kbd>
|
||||||
|
</div>
|
||||||
|
<div class="flex">
|
||||||
|
<kbd class="shortcut-key">?</kbd>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ButtonSecondary
|
||||||
|
:label="`${t('app.documentation')}`"
|
||||||
|
to="https://docs.hoppscotch.io/realtime"
|
||||||
|
:icon="IconExternalLink"
|
||||||
|
blank
|
||||||
|
outline
|
||||||
|
reverse
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -61,8 +106,10 @@ import IconTrash from "~icons/lucide/trash"
|
|||||||
import IconArrowUp from "~icons/lucide/arrow-up"
|
import IconArrowUp from "~icons/lucide/arrow-up"
|
||||||
import IconArrowDown from "~icons/lucide/arrow-down"
|
import IconArrowDown from "~icons/lucide/arrow-down"
|
||||||
import IconChevronsDown from "~icons/lucide/chevron-down"
|
import IconChevronsDown from "~icons/lucide/chevron-down"
|
||||||
|
import IconExternalLink from "~icons/lucide/external-link"
|
||||||
import { useThrottleFn, useScroll } from "@vueuse/core"
|
import { useThrottleFn, useScroll } from "@vueuse/core"
|
||||||
import { useI18n } from "@composables/i18n"
|
import { useI18n } from "@composables/i18n"
|
||||||
|
import { getPlatformSpecialKey as getSpecialKey } from "~/helpers/platformutils"
|
||||||
|
|
||||||
export type LogEntryData = {
|
export type LogEntryData = {
|
||||||
prefix?: string
|
prefix?: string
|
||||||
|
|||||||
@@ -371,6 +371,7 @@ const icon = computed(() => markRaw(ICONS[props.entry.source].icon))
|
|||||||
.realtime-log {
|
.realtime-log {
|
||||||
@apply text-secondary;
|
@apply text-secondary;
|
||||||
@apply overflow-hidden;
|
@apply overflow-hidden;
|
||||||
|
@apply hover:cursor-nsResize;
|
||||||
|
|
||||||
&,
|
&,
|
||||||
span {
|
span {
|
||||||
|
|||||||
138
packages/hoppscotch-app/src/components/realtime/Subscription.vue
Normal file
138
packages/hoppscotch-app/src/components/realtime/Subscription.vue
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
<template>
|
||||||
|
<SmartModal v-if="show" dialog :title="t('mqtt.new')" @close="hideModal">
|
||||||
|
<template #body>
|
||||||
|
<div class="flex justify-between mb-4">
|
||||||
|
<div
|
||||||
|
class="flex items-center border divide-x rounded border-divider divide-divider"
|
||||||
|
>
|
||||||
|
<label class="mx-4">
|
||||||
|
{{ t("mqtt.qos") }}
|
||||||
|
</label>
|
||||||
|
<tippy interactive trigger="click" theme="popover">
|
||||||
|
<span class="select-wrapper">
|
||||||
|
<ButtonSecondary class="pr-8" :label="`${QoS}`" />
|
||||||
|
</span>
|
||||||
|
<template #content="{ hide }">
|
||||||
|
<div class="flex flex-col" role="menu">
|
||||||
|
<SmartItem
|
||||||
|
v-for="item in QOS_VALUES"
|
||||||
|
:key="`qos-${item}`"
|
||||||
|
:label="`${item}`"
|
||||||
|
:icon="QoS === item ? IconCheckCircle : IconCircle"
|
||||||
|
:active="QoS === item"
|
||||||
|
@click="
|
||||||
|
() => {
|
||||||
|
QoS = item
|
||||||
|
hide()
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</tippy>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<label for="select-color">
|
||||||
|
{{ t("mqtt.color") }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="select-color"
|
||||||
|
v-model="color"
|
||||||
|
type="color"
|
||||||
|
class="w-8 h-8 p-1 ml-4 border rounded bg-primary border-divider"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<input
|
||||||
|
id="selectLabelAdd"
|
||||||
|
v-model="name"
|
||||||
|
v-focus
|
||||||
|
class="input floating-input"
|
||||||
|
placeholder=" "
|
||||||
|
type="text"
|
||||||
|
autocomplete="off"
|
||||||
|
@keyup.enter="addNewSubscription"
|
||||||
|
/>
|
||||||
|
<label for="selectLabelAdd">
|
||||||
|
{{ t("action.label") }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #footer>
|
||||||
|
<span class="flex space-x-2">
|
||||||
|
<ButtonPrimary
|
||||||
|
:label="t('mqtt.subscribe')"
|
||||||
|
:loading="loadingState"
|
||||||
|
outline
|
||||||
|
@click="addNewSubscription"
|
||||||
|
/>
|
||||||
|
<ButtonSecondary
|
||||||
|
:label="t('action.cancel')"
|
||||||
|
outline
|
||||||
|
filled
|
||||||
|
@click="hideModal"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
</SmartModal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import IconCheckCircle from "~icons/lucide/check-circle"
|
||||||
|
import IconCircle from "~icons/lucide/circle"
|
||||||
|
import { ref, watch } from "vue"
|
||||||
|
import { MQTTTopic, QOS_VALUES } from "~/helpers/realtime/MQTTConnection"
|
||||||
|
import { useI18n } from "@composables/i18n"
|
||||||
|
import { useToast } from "@composables/toast"
|
||||||
|
|
||||||
|
const toastr = useToast()
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
loadingState: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "hide-modal"): void
|
||||||
|
(e: "submit", body: MQTTTopic): void
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const QoS = ref<typeof QOS_VALUES[number]>(2)
|
||||||
|
const name = ref("")
|
||||||
|
const color = ref("#f58290")
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.show,
|
||||||
|
() => {
|
||||||
|
name.value = ""
|
||||||
|
QoS.value = 2
|
||||||
|
const randomColor = Math.floor(Math.random() * 16777215).toString(16)
|
||||||
|
color.value = `#${randomColor}`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const addNewSubscription = () => {
|
||||||
|
if (!name.value) {
|
||||||
|
toastr.error(t("mqtt.invalid_topic").toString())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
emit("submit", {
|
||||||
|
name: name.value,
|
||||||
|
qos: QoS.value,
|
||||||
|
color: color.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const hideModal = () => {
|
||||||
|
name.value = ""
|
||||||
|
QoS.value = 2
|
||||||
|
emit("hide-modal")
|
||||||
|
}
|
||||||
|
</script>
|
||||||
49
packages/hoppscotch-app/src/components/smart/Window.vue
Normal file
49
packages/hoppscotch-app/src/components/smart/Window.vue
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<div v-show="active" class="flex flex-col flex-1 overflow-y-auto">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import {
|
||||||
|
onMounted,
|
||||||
|
onBeforeUnmount,
|
||||||
|
inject,
|
||||||
|
computed,
|
||||||
|
watch,
|
||||||
|
useSlots,
|
||||||
|
} from "vue"
|
||||||
|
import { TabMeta, TabProvider } from "./Windows.vue"
|
||||||
|
|
||||||
|
const slots = useSlots()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
label: { type: String, default: null },
|
||||||
|
info: { type: String, default: null },
|
||||||
|
id: { type: String, default: null, required: true },
|
||||||
|
isRemovable: { type: Boolean, default: true },
|
||||||
|
selected: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const tabMeta = computed<TabMeta>(() => ({
|
||||||
|
info: props.info,
|
||||||
|
label: props.label,
|
||||||
|
isRemovable: props.isRemovable,
|
||||||
|
icon: slots.icon,
|
||||||
|
}))
|
||||||
|
const { activeTabID, addTabEntry, updateTabEntry, removeTabEntry } =
|
||||||
|
inject<TabProvider>("tabs-system")!
|
||||||
|
const active = computed(() => activeTabID.value === props.id)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
addTabEntry(props.id, tabMeta.value)
|
||||||
|
})
|
||||||
|
watch(tabMeta, (newMeta) => {
|
||||||
|
updateTabEntry(props.id, newMeta)
|
||||||
|
})
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
removeTabEntry(props.id)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
274
packages/hoppscotch-app/src/components/smart/Windows.vue
Normal file
274
packages/hoppscotch-app/src/components/smart/Windows.vue
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col flex-1 h-auto overflow-y-hidden flex-nowrap">
|
||||||
|
<div class="relative sticky top-0 z-10 tabs bg-primaryLight">
|
||||||
|
<div class="flex flex-1 w-0 overflow-x-auto">
|
||||||
|
<div class="flex justify-between divide-x divide-dividerLight">
|
||||||
|
<div class="flex">
|
||||||
|
<draggable
|
||||||
|
v-bind="dragOptions"
|
||||||
|
:list="tabEntries"
|
||||||
|
:style="tabsWidth"
|
||||||
|
:item-key="'window-'"
|
||||||
|
class="flex overflow-x-auto transition divide-x divide-dividerLight"
|
||||||
|
@sort="sortTabs"
|
||||||
|
>
|
||||||
|
<template #item="{ element: [tabID, tabMeta] }">
|
||||||
|
<button
|
||||||
|
:key="`removable-tab-${tabID}`"
|
||||||
|
class="tab"
|
||||||
|
:class="[{ active: modelValue === tabID }]"
|
||||||
|
:aria-label="tabMeta.label || ''"
|
||||||
|
role="button"
|
||||||
|
@keyup.enter="selectTab(tabID)"
|
||||||
|
@click="selectTab(tabID)"
|
||||||
|
>
|
||||||
|
<div class="flex items-stretch truncate">
|
||||||
|
<span
|
||||||
|
v-if="tabMeta.icon"
|
||||||
|
class="flex items-center justify-center mx-4 cursor-pointer"
|
||||||
|
>
|
||||||
|
<component :is="tabMeta.icon" class="w-4 h-4 svg-icons" />
|
||||||
|
</span>
|
||||||
|
<span class="truncate">
|
||||||
|
{{ tabMeta.label }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip', delay: [500, 20] }"
|
||||||
|
:icon="IconX"
|
||||||
|
:style="{
|
||||||
|
visibility: tabMeta.isRemovable ? 'visible' : 'hidden',
|
||||||
|
}"
|
||||||
|
:title="t('action.close')"
|
||||||
|
:class="[{ active: modelValue === tabID }, 'close']"
|
||||||
|
class="rounded mx-2 !py-0.5 !px-1"
|
||||||
|
@click.stop="emit('removeTab', tabID)"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</draggable>
|
||||||
|
</div>
|
||||||
|
<div class="sticky right-0 flex items-center justify-center z-8">
|
||||||
|
<slot name="actions">
|
||||||
|
<span
|
||||||
|
v-if="canAddNewTab"
|
||||||
|
class="flex items-center justify-center px-2 py-1.5 bg-primaryLight z-8"
|
||||||
|
>
|
||||||
|
<ButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:title="t('action.new')"
|
||||||
|
:icon="IconPlus"
|
||||||
|
class="rounded !p-1"
|
||||||
|
filled
|
||||||
|
@click="addTab"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="w-full h-full contents">
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import IconPlus from "~icons/lucide/plus"
|
||||||
|
import IconX from "~icons/lucide/x"
|
||||||
|
import { pipe } from "fp-ts/function"
|
||||||
|
import { not } from "fp-ts/Predicate"
|
||||||
|
import * as A from "fp-ts/Array"
|
||||||
|
import * as O from "fp-ts/Option"
|
||||||
|
import { ref, ComputedRef, computed, provide } from "vue"
|
||||||
|
import type { Slot } from "vue"
|
||||||
|
import draggable from "vuedraggable"
|
||||||
|
import { throwError } from "~/helpers/functional/error"
|
||||||
|
import { useI18n } from "~/composables/i18n"
|
||||||
|
|
||||||
|
export type TabMeta = {
|
||||||
|
label: string | null
|
||||||
|
icon: Slot | undefined
|
||||||
|
info: string | null
|
||||||
|
isRemovable: boolean
|
||||||
|
}
|
||||||
|
export type TabProvider = {
|
||||||
|
activeTabID: ComputedRef<string>
|
||||||
|
addTabEntry: (tabID: string, meta: TabMeta) => void
|
||||||
|
updateTabEntry: (tabID: string, newMeta: TabMeta) => void
|
||||||
|
removeTabEntry: (tabID: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const t = useI18n()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
styles: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
canAddNewTab: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: "update:modelValue", newTabID: string): void
|
||||||
|
(e: "sort", body: { oldIndex: number; newIndex: number }): void
|
||||||
|
(e: "removeTab", tabID: string): void
|
||||||
|
(e: "addTab"): void
|
||||||
|
}>()
|
||||||
|
const tabEntries = ref<Array<[string, TabMeta]>>([])
|
||||||
|
const tabsWidth = computed(() => ({
|
||||||
|
maxWidth: `${tabEntries.value.length * 184}px`,
|
||||||
|
width: "100%",
|
||||||
|
minWidth: "0px",
|
||||||
|
transition: "max-width 0.2s",
|
||||||
|
}))
|
||||||
|
const dragOptions = {
|
||||||
|
group: "tabs",
|
||||||
|
animation: 250,
|
||||||
|
handle: ".tab",
|
||||||
|
draggable: ".tab",
|
||||||
|
ghostClass: "cursor-move",
|
||||||
|
}
|
||||||
|
const addTabEntry = (tabID: string, meta: TabMeta) => {
|
||||||
|
tabEntries.value = pipe(
|
||||||
|
tabEntries.value,
|
||||||
|
O.fromPredicate(not(A.exists(([id]) => id === tabID))),
|
||||||
|
O.map(A.append([tabID, meta] as [string, TabMeta])),
|
||||||
|
O.getOrElseW(() => throwError(`Tab with duplicate ID created: '${tabID}'`))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const updateTabEntry = (tabID: string, newMeta: TabMeta) => {
|
||||||
|
tabEntries.value = pipe(
|
||||||
|
tabEntries.value,
|
||||||
|
A.findIndex(([id]) => id === tabID),
|
||||||
|
O.chain((index) =>
|
||||||
|
pipe(
|
||||||
|
tabEntries.value,
|
||||||
|
A.updateAt(index, [tabID, newMeta] as [string, TabMeta])
|
||||||
|
)
|
||||||
|
),
|
||||||
|
O.getOrElseW(() => throwError(`Failed to update tab entry: ${tabID}`))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const removeTabEntry = (tabID: string) => {
|
||||||
|
tabEntries.value = pipe(
|
||||||
|
tabEntries.value,
|
||||||
|
A.findIndex(([id]) => id === tabID),
|
||||||
|
O.chain((index) => pipe(tabEntries.value, A.deleteAt(index))),
|
||||||
|
O.getOrElseW(() => throwError(`Failed to remove tab entry: ${tabID}`))
|
||||||
|
)
|
||||||
|
// If we tried to remove the active tabEntries, switch to first tab entry
|
||||||
|
if (props.modelValue === tabID)
|
||||||
|
if (tabEntries.value.length > 0) selectTab(tabEntries.value[0][0])
|
||||||
|
}
|
||||||
|
const sortTabs = (e: {
|
||||||
|
oldDraggableIndex: number
|
||||||
|
newDraggableIndex: number
|
||||||
|
}) => {
|
||||||
|
emit("sort", {
|
||||||
|
oldIndex: e.oldDraggableIndex,
|
||||||
|
newIndex: e.newDraggableIndex,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
provide<TabProvider>("tabs-system", {
|
||||||
|
activeTabID: computed(() => props.modelValue),
|
||||||
|
addTabEntry,
|
||||||
|
updateTabEntry,
|
||||||
|
removeTabEntry,
|
||||||
|
})
|
||||||
|
const selectTab = (id: string) => {
|
||||||
|
emit("update:modelValue", id)
|
||||||
|
}
|
||||||
|
const addTab = () => {
|
||||||
|
emit("addTab")
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.tabs {
|
||||||
|
@apply flex;
|
||||||
|
@apply whitespace-nowrap;
|
||||||
|
@apply overflow-auto;
|
||||||
|
@apply flex-shrink-0;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
@apply absolute;
|
||||||
|
@apply inset-x-0;
|
||||||
|
@apply bottom-0;
|
||||||
|
@apply bg-dividerLight;
|
||||||
|
@apply z-10;
|
||||||
|
@apply h-0.25;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
@apply relative;
|
||||||
|
@apply flex;
|
||||||
|
@apply py-2;
|
||||||
|
@apply font-semibold;
|
||||||
|
@apply w-46;
|
||||||
|
@apply transition;
|
||||||
|
@apply flex-1;
|
||||||
|
@apply items-center;
|
||||||
|
@apply justify-between;
|
||||||
|
@apply text-secondaryLight;
|
||||||
|
@apply hover:bg-primaryDark;
|
||||||
|
@apply hover:text-secondary;
|
||||||
|
@apply focus-visible:text-secondaryDark;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
@apply absolute;
|
||||||
|
@apply left-0;
|
||||||
|
@apply right-0;
|
||||||
|
@apply top-0;
|
||||||
|
@apply bg-transparent;
|
||||||
|
@apply z-2;
|
||||||
|
@apply h-0.5;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// &::after {
|
||||||
|
// @apply absolute;
|
||||||
|
// @apply left-0;
|
||||||
|
// @apply right-0;
|
||||||
|
// @apply bottom-0;
|
||||||
|
// @apply bg-divider;
|
||||||
|
// @apply z-2;
|
||||||
|
// @apply h-0.25;
|
||||||
|
// content: "";
|
||||||
|
// }
|
||||||
|
|
||||||
|
&:focus::before {
|
||||||
|
@apply bg-divider;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
@apply text-secondaryDark;
|
||||||
|
@apply bg-primary;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
@apply bg-accent;
|
||||||
|
}
|
||||||
|
|
||||||
|
// &::after {
|
||||||
|
// @apply bg-transparent;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
@apply opacity-50;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
@apply opacity-80;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -68,7 +68,7 @@ type HoppActionWithArgs = keyof HoppActionArgs
|
|||||||
/**
|
/**
|
||||||
* HoppActions which do not require arguments for their invocation
|
* HoppActions which do not require arguments for their invocation
|
||||||
*/
|
*/
|
||||||
type HoppActionWithNoArgs = Exclude<HoppAction, HoppActionWithArgs>
|
export type HoppActionWithNoArgs = Exclude<HoppAction, HoppActionWithArgs>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the argument type for a given HoppAction
|
* Resolves the argument type for a given HoppAction
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { onBeforeUnmount, onMounted } from "vue"
|
import { onBeforeUnmount, onMounted } from "vue"
|
||||||
import { HoppAction, invokeAction } from "./actions"
|
import { HoppActionWithNoArgs, invokeAction } from "./actions"
|
||||||
import { isAppleDevice } from "./platformutils"
|
import { isAppleDevice } from "./platformutils"
|
||||||
import { isDOMElement, isTypableElement } from "./utils/dom"
|
import { isDOMElement, isTypableElement } from "./utils/dom"
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ type Key =
|
|||||||
| "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t"
|
| "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t"
|
||||||
| "u" | "v" | "w" | "x" | "y" | "z" | "0" | "1" | "2" | "3"
|
| "u" | "v" | "w" | "x" | "y" | "z" | "0" | "1" | "2" | "3"
|
||||||
| "4" | "5" | "6" | "7" | "8" | "9" | "up" | "down" | "left"
|
| "4" | "5" | "6" | "7" | "8" | "9" | "up" | "down" | "left"
|
||||||
| "right" | "/" | "?" | "."
|
| "right" | "/" | "?" | "." | "enter"
|
||||||
/* eslint-enable */
|
/* eslint-enable */
|
||||||
|
|
||||||
type ModifierBasedShortcutKey = `${ModifierKeys}-${Key}`
|
type ModifierBasedShortcutKey = `${ModifierKeys}-${Key}`
|
||||||
@@ -34,9 +34,9 @@ type ShortcutKey = ModifierBasedShortcutKey | SingleCharacterShortcutKey
|
|||||||
|
|
||||||
export const bindings: {
|
export const bindings: {
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
[_ in ShortcutKey]?: HoppAction
|
[_ in ShortcutKey]?: HoppActionWithNoArgs
|
||||||
} = {
|
} = {
|
||||||
"ctrl-g": "request.send-cancel",
|
"ctrl-enter": "request.send-cancel",
|
||||||
"ctrl-i": "request.reset",
|
"ctrl-i": "request.reset",
|
||||||
"ctrl-u": "request.copy-link",
|
"ctrl-u": "request.copy-link",
|
||||||
"ctrl-s": "request.save",
|
"ctrl-s": "request.save",
|
||||||
@@ -136,6 +136,8 @@ function getPressedKey(ev: KeyboardEvent): Key | null {
|
|||||||
// Check if period
|
// Check if period
|
||||||
if (val === ".") return "."
|
if (val === ".") return "."
|
||||||
|
|
||||||
|
if (val === "enter") return "enter"
|
||||||
|
|
||||||
// If no other cases match, this is not a valid key
|
// If no other cases match, this is not a valid key
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,17 @@ import Paho, { ConnectionOptions } from "paho-mqtt"
|
|||||||
import { BehaviorSubject, Subject } from "rxjs"
|
import { BehaviorSubject, Subject } from "rxjs"
|
||||||
import { logHoppRequestRunToAnalytics } from "../fb/analytics"
|
import { logHoppRequestRunToAnalytics } from "../fb/analytics"
|
||||||
|
|
||||||
|
export type MQTTConnectionConfig = {
|
||||||
|
username?: string
|
||||||
|
password?: string
|
||||||
|
keepAlive?: string
|
||||||
|
cleanSession?: boolean
|
||||||
|
lwTopic?: string
|
||||||
|
lwMessage: string
|
||||||
|
lwQos: 2 | 1 | 0
|
||||||
|
lwRetain: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export type MQTTMessage = { topic: string; message: string }
|
export type MQTTMessage = { topic: string; message: string }
|
||||||
export type MQTTError =
|
export type MQTTError =
|
||||||
| { type: "CONNECTION_NOT_ESTABLISHED"; value: unknown }
|
| { type: "CONNECTION_NOT_ESTABLISHED"; value: unknown }
|
||||||
@@ -21,12 +32,22 @@ export type MQTTEvent = { time: number } & (
|
|||||||
| { type: "ERROR"; error: MQTTError }
|
| { type: "ERROR"; error: MQTTError }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export type MQTTTopic = {
|
||||||
|
name: string
|
||||||
|
color: string
|
||||||
|
qos: 2 | 1 | 0
|
||||||
|
}
|
||||||
|
|
||||||
export type ConnectionState = "CONNECTING" | "CONNECTED" | "DISCONNECTED"
|
export type ConnectionState = "CONNECTING" | "CONNECTED" | "DISCONNECTED"
|
||||||
|
|
||||||
|
export const QOS_VALUES = [2, 1, 0] as const
|
||||||
|
|
||||||
export class MQTTConnection {
|
export class MQTTConnection {
|
||||||
|
subscribing$ = new BehaviorSubject(false)
|
||||||
subscriptionState$ = new BehaviorSubject<boolean>(false)
|
subscriptionState$ = new BehaviorSubject<boolean>(false)
|
||||||
connectionState$ = new BehaviorSubject<ConnectionState>("DISCONNECTED")
|
connectionState$ = new BehaviorSubject<ConnectionState>("DISCONNECTED")
|
||||||
event$: Subject<MQTTEvent> = new Subject()
|
event$: Subject<MQTTEvent> = new Subject()
|
||||||
|
subscribedTopics$ = new BehaviorSubject<MQTTTopic[]>([])
|
||||||
|
|
||||||
private mqttClient: Paho.Client | undefined
|
private mqttClient: Paho.Client | undefined
|
||||||
private manualDisconnect = false
|
private manualDisconnect = false
|
||||||
@@ -35,7 +56,7 @@ export class MQTTConnection {
|
|||||||
this.event$.next(event)
|
this.event$.next(event)
|
||||||
}
|
}
|
||||||
|
|
||||||
connect(url: string, username: string, password: string) {
|
connect(url: string, clientID: string, config: MQTTConnectionConfig) {
|
||||||
try {
|
try {
|
||||||
this.connectionState$.next("CONNECTING")
|
this.connectionState$.next("CONNECTING")
|
||||||
|
|
||||||
@@ -49,19 +70,34 @@ export class MQTTConnection {
|
|||||||
this.mqttClient = new Paho.Client(
|
this.mqttClient = new Paho.Client(
|
||||||
`${hostname + (pathname !== "/" ? pathname : "")}`,
|
`${hostname + (pathname !== "/" ? pathname : "")}`,
|
||||||
port !== "" ? Number(port) : 8081,
|
port !== "" ? Number(port) : 8081,
|
||||||
"hoppscotch"
|
clientID ?? "hoppscotch"
|
||||||
)
|
)
|
||||||
const connectOptions: ConnectionOptions = {
|
const connectOptions: ConnectionOptions = {
|
||||||
onSuccess: this.onConnectionSuccess.bind(this),
|
onSuccess: this.onConnectionSuccess.bind(this),
|
||||||
onFailure: this.onConnectionFailure.bind(this),
|
onFailure: this.onConnectionFailure.bind(this),
|
||||||
|
timeout: 3,
|
||||||
|
keepAliveInterval: Number(config.keepAlive) ?? 60,
|
||||||
|
cleanSession: config.cleanSession ?? true,
|
||||||
useSSL: parseUrl.protocol !== "ws:",
|
useSSL: parseUrl.protocol !== "ws:",
|
||||||
}
|
}
|
||||||
if (username !== "") {
|
|
||||||
|
const { username, password, lwTopic, lwMessage, lwQos, lwRetain } = config
|
||||||
|
|
||||||
|
if (username) {
|
||||||
connectOptions.userName = username
|
connectOptions.userName = username
|
||||||
}
|
}
|
||||||
if (password !== "") {
|
if (password) {
|
||||||
connectOptions.password = password
|
connectOptions.password = password
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (lwTopic?.length) {
|
||||||
|
const willmsg = new Paho.Message(lwMessage)
|
||||||
|
willmsg.qos = lwQos
|
||||||
|
willmsg.destinationName = lwTopic
|
||||||
|
willmsg.retained = lwRetain
|
||||||
|
connectOptions.willMessage = willmsg
|
||||||
|
}
|
||||||
|
|
||||||
this.mqttClient.connect(connectOptions)
|
this.mqttClient.connect(connectOptions)
|
||||||
this.mqttClient.onConnectionLost = this.onConnectionLost.bind(this)
|
this.mqttClient.onConnectionLost = this.onConnectionLost.bind(this)
|
||||||
this.mqttClient.onMessageArrived = this.onMessageArrived.bind(this)
|
this.mqttClient.onMessageArrived = this.onMessageArrived.bind(this)
|
||||||
@@ -112,6 +148,7 @@ export class MQTTConnection {
|
|||||||
}
|
}
|
||||||
this.manualDisconnect = false
|
this.manualDisconnect = false
|
||||||
this.subscriptionState$.next(false)
|
this.subscriptionState$.next(false)
|
||||||
|
this.subscribedTopics$.next([])
|
||||||
}
|
}
|
||||||
|
|
||||||
onMessageArrived({
|
onMessageArrived({
|
||||||
@@ -170,34 +207,45 @@ export class MQTTConnection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
subscribe(topic: string) {
|
subscribe(topic: MQTTTopic) {
|
||||||
|
this.subscribing$.next(true)
|
||||||
try {
|
try {
|
||||||
this.mqttClient?.subscribe(topic, {
|
this.mqttClient?.subscribe(topic.name, {
|
||||||
onSuccess: this.usubSuccess.bind(this, topic),
|
onSuccess: this.subSuccess.bind(this, topic),
|
||||||
onFailure: this.usubFailure.bind(this, topic),
|
onFailure: this.usubFailure.bind(this, topic.name),
|
||||||
|
qos: topic.qos,
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
this.subscribing$.next(false)
|
||||||
this.addEvent({
|
this.addEvent({
|
||||||
time: Date.now(),
|
time: Date.now(),
|
||||||
type: "ERROR",
|
type: "ERROR",
|
||||||
error: {
|
error: {
|
||||||
type: "SUBSCRIPTION_FAILED",
|
type: "SUBSCRIPTION_FAILED",
|
||||||
topic,
|
topic: topic.name,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
usubSuccess(topic: string) {
|
subSuccess(topic: MQTTTopic) {
|
||||||
|
this.subscribing$.next(false)
|
||||||
this.subscriptionState$.next(!this.subscriptionState$.value)
|
this.subscriptionState$.next(!this.subscriptionState$.value)
|
||||||
|
this.addSubscription(topic)
|
||||||
this.addEvent({
|
this.addEvent({
|
||||||
time: Date.now(),
|
time: Date.now(),
|
||||||
type: "SUBSCRIBED",
|
type: "SUBSCRIBED",
|
||||||
topic,
|
topic: topic.name,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
usubSuccess(topic: string) {
|
||||||
|
this.subscribing$.next(false)
|
||||||
|
this.removeSubscription(topic)
|
||||||
|
}
|
||||||
|
|
||||||
usubFailure(topic: string) {
|
usubFailure(topic: string) {
|
||||||
|
this.subscribing$.next(false)
|
||||||
this.addEvent({
|
this.addEvent({
|
||||||
time: Date.now(),
|
time: Date.now(),
|
||||||
type: "ERROR",
|
type: "ERROR",
|
||||||
@@ -215,6 +263,21 @@ export class MQTTConnection {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addSubscription(topic: MQTTTopic) {
|
||||||
|
const subscriptions = this.subscribedTopics$.getValue()
|
||||||
|
subscriptions.push({
|
||||||
|
name: topic.name,
|
||||||
|
color: topic.color,
|
||||||
|
qos: topic.qos,
|
||||||
|
})
|
||||||
|
this.subscribedTopics$.next(subscriptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
removeSubscription(topic: string) {
|
||||||
|
const subscriptions = this.subscribedTopics$.getValue()
|
||||||
|
this.subscribedTopics$.next(subscriptions.filter((t) => t.name !== topic))
|
||||||
|
}
|
||||||
|
|
||||||
disconnect() {
|
disconnect() {
|
||||||
this.manualDisconnect = true
|
this.manualDisconnect = true
|
||||||
this.mqttClient?.disconnect()
|
this.mqttClient?.disconnect()
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export default [
|
|||||||
section: "shortcut.request.title",
|
section: "shortcut.request.title",
|
||||||
shortcuts: [
|
shortcuts: [
|
||||||
{
|
{
|
||||||
keys: [getPlatformSpecialKey(), "G"],
|
keys: [getPlatformSpecialKey(), "Enter"],
|
||||||
label: "shortcut.request.send_request",
|
label: "shortcut.request.send_request",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,8 +6,17 @@ import {
|
|||||||
HoppRealtimeLogLine,
|
HoppRealtimeLogLine,
|
||||||
} from "~/helpers/types/HoppRealtimeLog"
|
} from "~/helpers/types/HoppRealtimeLog"
|
||||||
|
|
||||||
|
type MQTTTab = {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
color: string
|
||||||
|
removable: boolean
|
||||||
|
logs: HoppRealtimeLog[]
|
||||||
|
}
|
||||||
|
|
||||||
type HoppMQTTRequest = {
|
type HoppMQTTRequest = {
|
||||||
endpoint: string
|
endpoint: string
|
||||||
|
clientID: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type HoppMQTTSession = {
|
type HoppMQTTSession = {
|
||||||
@@ -15,10 +24,21 @@ type HoppMQTTSession = {
|
|||||||
subscriptionState: boolean
|
subscriptionState: boolean
|
||||||
log: HoppRealtimeLog
|
log: HoppRealtimeLog
|
||||||
socket: MQTTConnection
|
socket: MQTTConnection
|
||||||
|
tabs: MQTTTab[]
|
||||||
|
currentTabId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultMQTTRequest: HoppMQTTRequest = {
|
const defaultMQTTRequest: HoppMQTTRequest = {
|
||||||
endpoint: "wss://test.mosquitto.org:8081",
|
endpoint: "wss://test.mosquitto.org:8081",
|
||||||
|
clientID: "hoppscotch",
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultTab: MQTTTab = {
|
||||||
|
id: "all",
|
||||||
|
name: "All Topics",
|
||||||
|
color: "var(--accent-color)",
|
||||||
|
removable: false,
|
||||||
|
logs: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultMQTTSession: HoppMQTTSession = {
|
const defaultMQTTSession: HoppMQTTSession = {
|
||||||
@@ -26,6 +46,8 @@ const defaultMQTTSession: HoppMQTTSession = {
|
|||||||
subscriptionState: false,
|
subscriptionState: false,
|
||||||
socket: new MQTTConnection(),
|
socket: new MQTTConnection(),
|
||||||
log: [],
|
log: [],
|
||||||
|
tabs: [defaultTab],
|
||||||
|
currentTabId: defaultTab.id,
|
||||||
}
|
}
|
||||||
|
|
||||||
const dispatchers = defineDispatchers({
|
const dispatchers = defineDispatchers({
|
||||||
@@ -37,13 +59,22 @@ const dispatchers = defineDispatchers({
|
|||||||
request: newRequest,
|
request: newRequest,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
setEndpoint(_: HoppMQTTSession, { newEndpoint }: { newEndpoint: string }) {
|
setEndpoint(curr: HoppMQTTSession, { newEndpoint }: { newEndpoint: string }) {
|
||||||
return {
|
return {
|
||||||
request: {
|
request: {
|
||||||
|
clientID: curr.request.clientID,
|
||||||
endpoint: newEndpoint,
|
endpoint: newEndpoint,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setClientID(curr: HoppMQTTSession, { newClientID }: { newClientID: string }) {
|
||||||
|
return {
|
||||||
|
request: {
|
||||||
|
endpoint: curr.request.endpoint,
|
||||||
|
clientID: newClientID,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
setConn(_: HoppMQTTSession, { socket }: { socket: MQTTConnection }) {
|
setConn(_: HoppMQTTSession, { socket }: { socket: MQTTConnection }) {
|
||||||
return {
|
return {
|
||||||
socket,
|
socket,
|
||||||
@@ -64,6 +95,47 @@ const dispatchers = defineDispatchers({
|
|||||||
log: [...curr.log, line],
|
log: [...curr.log, line],
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setTabs(_: HoppMQTTSession, { tabs }: { tabs: MQTTTab[] }) {
|
||||||
|
return {
|
||||||
|
tabs,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addTab(curr: HoppMQTTSession, { tab }: { tab: MQTTTab }) {
|
||||||
|
return {
|
||||||
|
tabs: [...curr.tabs, tab],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setCurrentTabId(_: HoppMQTTSession, { tabId }: { tabId: string }) {
|
||||||
|
return {
|
||||||
|
currentTabId: tabId,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setCurrentTabLog(
|
||||||
|
_: HoppMQTTSession,
|
||||||
|
{ log, tabId }: { log: HoppRealtimeLog[]; tabId: string }
|
||||||
|
) {
|
||||||
|
const newTabs = _.tabs.map((tab) => {
|
||||||
|
if (tab.id === tabId) tab.logs = log
|
||||||
|
return tab
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
tabs: newTabs,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addCurrentTabLogLine(
|
||||||
|
_: HoppMQTTSession,
|
||||||
|
{ line, tabId }: { tabId: string; line: HoppRealtimeLog }
|
||||||
|
) {
|
||||||
|
const newTabs = _.tabs.map((tab) => {
|
||||||
|
if (tab.id === tabId) tab.logs = [...tab.logs, line]
|
||||||
|
return tab
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
tabs: newTabs,
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const MQTTSessionStore = new DispatchingStore(defaultMQTTSession, dispatchers)
|
const MQTTSessionStore = new DispatchingStore(defaultMQTTSession, dispatchers)
|
||||||
@@ -86,6 +158,15 @@ export function setMQTTEndpoint(newEndpoint: string) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setMQTTClientID(newClientID: string) {
|
||||||
|
MQTTSessionStore.dispatch({
|
||||||
|
dispatcher: "setClientID",
|
||||||
|
payload: {
|
||||||
|
newClientID,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function setMQTTConn(socket: MQTTConnection) {
|
export function setMQTTConn(socket: MQTTConnection) {
|
||||||
MQTTSessionStore.dispatch({
|
MQTTSessionStore.dispatch({
|
||||||
dispatcher: "setConn",
|
dispatcher: "setConn",
|
||||||
@@ -122,6 +203,56 @@ export function addMQTTLogLine(line: HoppRealtimeLogLine) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function setMQTTTabs(tabs: MQTTTab[]) {
|
||||||
|
MQTTSessionStore.dispatch({
|
||||||
|
dispatcher: "setTabs",
|
||||||
|
payload: {
|
||||||
|
tabs,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addMQTTTab(tab: MQTTTab) {
|
||||||
|
MQTTSessionStore.dispatch({
|
||||||
|
dispatcher: "addTab",
|
||||||
|
payload: {
|
||||||
|
tab,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setCurrentTab(tabId: string) {
|
||||||
|
MQTTSessionStore.dispatch({
|
||||||
|
dispatcher: "setCurrentTabId",
|
||||||
|
payload: {
|
||||||
|
tabId,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setMQTTCurrentTabLog(tabId: string, log: HoppRealtimeLog) {
|
||||||
|
MQTTSessionStore.dispatch({
|
||||||
|
dispatcher: "setCurrentTabLog",
|
||||||
|
payload: {
|
||||||
|
tabId,
|
||||||
|
log,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addMQTTCurrentTabLogLine(
|
||||||
|
tabId: string,
|
||||||
|
line: HoppRealtimeLogLine
|
||||||
|
) {
|
||||||
|
MQTTSessionStore.dispatch({
|
||||||
|
dispatcher: "addCurrentTabLogLine",
|
||||||
|
payload: {
|
||||||
|
tabId,
|
||||||
|
line,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const MQTTRequest$ = MQTTSessionStore.subject$.pipe(
|
export const MQTTRequest$ = MQTTSessionStore.subject$.pipe(
|
||||||
pluck("request"),
|
pluck("request"),
|
||||||
distinctUntilChanged()
|
distinctUntilChanged()
|
||||||
@@ -132,6 +263,11 @@ export const MQTTEndpoint$ = MQTTSessionStore.subject$.pipe(
|
|||||||
distinctUntilChanged()
|
distinctUntilChanged()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const MQTTClientID$ = MQTTSessionStore.subject$.pipe(
|
||||||
|
pluck("request", "clientID"),
|
||||||
|
distinctUntilChanged()
|
||||||
|
)
|
||||||
|
|
||||||
export const MQTTConnectingState$ = MQTTSessionStore.subject$.pipe(
|
export const MQTTConnectingState$ = MQTTSessionStore.subject$.pipe(
|
||||||
pluck("connectingState"),
|
pluck("connectingState"),
|
||||||
distinctUntilChanged()
|
distinctUntilChanged()
|
||||||
@@ -156,3 +292,13 @@ export const MQTTLog$ = MQTTSessionStore.subject$.pipe(
|
|||||||
pluck("log"),
|
pluck("log"),
|
||||||
distinctUntilChanged()
|
distinctUntilChanged()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export const MQTTTabs$ = MQTTSessionStore.subject$.pipe(
|
||||||
|
pluck("tabs"),
|
||||||
|
distinctUntilChanged()
|
||||||
|
)
|
||||||
|
|
||||||
|
export const MQTTCurrentTab$ = MQTTSessionStore.subject$.pipe(
|
||||||
|
pluck("currentTabId"),
|
||||||
|
distinctUntilChanged()
|
||||||
|
)
|
||||||
|
|||||||
@@ -2,23 +2,42 @@
|
|||||||
<AppPaneLayout layout-id="mqtt">
|
<AppPaneLayout layout-id="mqtt">
|
||||||
<template #primary>
|
<template #primary>
|
||||||
<div
|
<div
|
||||||
class="sticky top-0 z-10 flex flex-shrink-0 p-4 overflow-x-auto space-x-2 bg-primary"
|
class="sticky top-0 z-10 flex flex-shrink-0 p-4 space-x-2 overflow-x-auto bg-primary"
|
||||||
>
|
>
|
||||||
<div class="inline-flex flex-1 space-x-2">
|
<div class="inline-flex flex-1 space-x-2">
|
||||||
|
<div class="flex flex-1">
|
||||||
<input
|
<input
|
||||||
id="mqtt-url"
|
id="mqtt-url"
|
||||||
v-model="url"
|
v-model="url"
|
||||||
type="url"
|
type="url"
|
||||||
autocomplete="off"
|
autocomplete="off"
|
||||||
spellcheck="false"
|
:class="{ error: !isUrlValid }"
|
||||||
class="w-full px-4 py-2 border rounded bg-primaryLight border-divider text-secondaryDark"
|
class="flex flex-1 w-full px-4 py-2 border rounded-l bg-primaryLight border-divider text-secondaryDark"
|
||||||
:placeholder="t('mqtt.url')"
|
:placeholder="`${t('mqtt.url')}`"
|
||||||
:disabled="
|
:disabled="
|
||||||
connectionState === 'CONNECTED' ||
|
connectionState === 'CONNECTED' ||
|
||||||
connectionState === 'CONNECTING'
|
connectionState === 'CONNECTING'
|
||||||
"
|
"
|
||||||
@keyup.enter="isUrlValid ? toggleConnection() : null"
|
@keyup.enter="isUrlValid ? toggleConnection() : null"
|
||||||
/>
|
/>
|
||||||
|
<label
|
||||||
|
for="client-id"
|
||||||
|
class="px-4 py-2 font-semibold truncate border-t border-b bg-primaryLight border-divider text-secondaryLight"
|
||||||
|
>
|
||||||
|
{{ t("mqtt.client_id") }}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="client-id"
|
||||||
|
v-model="clientID"
|
||||||
|
class="flex flex-1 w-full px-4 py-2 border rounded-r bg-primaryLight border-divider text-secondaryDark"
|
||||||
|
spellcheck="false"
|
||||||
|
:disabled="
|
||||||
|
connectionState === 'CONNECTED' ||
|
||||||
|
connectionState === 'CONNECTING'
|
||||||
|
"
|
||||||
|
@keyup.enter="isUrlValid ? toggleConnection() : null"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<ButtonPrimary
|
<ButtonPrimary
|
||||||
id="connect"
|
id="connect"
|
||||||
:disabled="!isUrlValid"
|
:disabled="!isUrlValid"
|
||||||
@@ -34,133 +53,215 @@
|
|||||||
@click="toggleConnection"
|
@click="toggleConnection"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex space-x-2">
|
|
||||||
<input
|
|
||||||
id="mqtt-username"
|
|
||||||
v-model="username"
|
|
||||||
type="text"
|
|
||||||
spellcheck="false"
|
|
||||||
class="input"
|
|
||||||
:placeholder="t('authorization.username')"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
id="mqtt-password"
|
|
||||||
v-model="password"
|
|
||||||
type="password"
|
|
||||||
spellcheck="false"
|
|
||||||
class="input"
|
|
||||||
:placeholder="t('authorization.password')"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="flex flex-col flex-1"
|
||||||
|
:class="{ '!hidden': connectionState === 'CONNECTED' }"
|
||||||
|
>
|
||||||
|
<RealtimeConnectionConfig @change="onChangeConfig" />
|
||||||
</div>
|
</div>
|
||||||
|
<RealtimeCommunication
|
||||||
|
v-if="connectionState === 'CONNECTED'"
|
||||||
|
:show-event-field="currentTabId === 'all'"
|
||||||
|
:is-connected="connectionState === 'CONNECTED'"
|
||||||
|
event-field-styles="top-upperPrimaryStickyFold"
|
||||||
|
:sticky-header-styles="
|
||||||
|
currentTabId === 'all'
|
||||||
|
? 'top-upperSecondaryStickyFold'
|
||||||
|
: 'top-upperPrimaryStickyFold'
|
||||||
|
"
|
||||||
|
@send-message="
|
||||||
|
publish(
|
||||||
|
currentTabId === 'all'
|
||||||
|
? $event
|
||||||
|
: {
|
||||||
|
message: $event.message,
|
||||||
|
eventName: currentTabId,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #secondary>
|
<template #secondary>
|
||||||
<RealtimeLog
|
<SmartWindows
|
||||||
:title="t('mqtt.log')"
|
:id="'communication_tab'"
|
||||||
:log="log"
|
v-model="currentTabId"
|
||||||
@delete="clearLogEntries()"
|
:can-add-new-tab="false"
|
||||||
|
@remove-tab="removeTab"
|
||||||
|
@sort="sortTabs"
|
||||||
|
>
|
||||||
|
<SmartWindow
|
||||||
|
v-for="tab in tabs"
|
||||||
|
:id="tab.id"
|
||||||
|
:key="'removable_tab_' + tab.id"
|
||||||
|
:label="tab.name"
|
||||||
|
:is-removable="tab.removable"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<icon-lucide-rss
|
||||||
|
:style="{
|
||||||
|
color: tab.color,
|
||||||
|
}"
|
||||||
|
class="w-4 h-4 svg-icons"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
<RealtimeLog
|
||||||
|
:title="t('mqtt.log')"
|
||||||
|
:log="((tab.id === 'all' ? logs : tab.logs) as LogEntryData[])"
|
||||||
|
@delete="clearLogEntries()"
|
||||||
|
/>
|
||||||
|
</SmartWindow>
|
||||||
|
</SmartWindows>
|
||||||
|
</template>
|
||||||
<template #sidebar>
|
<template #sidebar>
|
||||||
<div class="flex items-center justify-between p-4">
|
|
||||||
<label for="pubTopic" class="font-semibold text-secondaryLight">
|
|
||||||
{{ t("mqtt.topic") }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="flex px-4">
|
|
||||||
<input
|
|
||||||
id="pubTopic"
|
|
||||||
v-model="pubTopic"
|
|
||||||
class="input"
|
|
||||||
:placeholder="t('mqtt.topic_name')"
|
|
||||||
type="text"
|
|
||||||
autocomplete="off"
|
|
||||||
spellcheck="false"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-between p-4">
|
|
||||||
<label for="mqtt-message" class="font-semibold text-secondaryLight">
|
|
||||||
{{ t("mqtt.communication") }}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="flex px-4 space-x-2">
|
|
||||||
<input
|
|
||||||
id="mqtt-message"
|
|
||||||
v-model="message"
|
|
||||||
class="input"
|
|
||||||
type="text"
|
|
||||||
autocomplete="off"
|
|
||||||
:placeholder="t('mqtt.message')"
|
|
||||||
spellcheck="false"
|
|
||||||
/>
|
|
||||||
<ButtonPrimary
|
|
||||||
id="publish"
|
|
||||||
name="get"
|
|
||||||
:disabled="!canPublish"
|
|
||||||
:label="t('mqtt.publish')"
|
|
||||||
@click="publish"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-between p-4 mt-4 border-t border-dividerLight"
|
class="sticky z-10 flex flex-col border-b divide-y rounded-t divide-dividerLight bg-primary border-dividerLight"
|
||||||
>
|
>
|
||||||
<label for="subTopic" class="font-semibold text-secondaryLight">
|
<div class="flex justify-between flex-1">
|
||||||
{{ t("mqtt.topic") }}
|
<ButtonSecondary
|
||||||
</label>
|
:icon="IconPlus"
|
||||||
</div>
|
:label="t('mqtt.new')"
|
||||||
<div class="flex px-4 space-x-2">
|
class="!rounded-none"
|
||||||
<input
|
@click="showSubscriptionModal(true)"
|
||||||
id="subTopic"
|
|
||||||
v-model="subTopic"
|
|
||||||
type="text"
|
|
||||||
autocomplete="off"
|
|
||||||
:placeholder="t('mqtt.topic_name')"
|
|
||||||
spellcheck="false"
|
|
||||||
class="input"
|
|
||||||
/>
|
/>
|
||||||
<ButtonPrimary
|
<span class="flex">
|
||||||
id="subscribe"
|
<ButtonSecondary
|
||||||
name="get"
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
:disabled="!canSubscribe"
|
to="https://docs.hoppscotch.io/features/mqtt"
|
||||||
:label="
|
blank
|
||||||
subscriptionState ? t('mqtt.unsubscribe') : t('mqtt.subscribe')
|
:title="t('app.wiki')"
|
||||||
"
|
:icon="IconHelpCircle"
|
||||||
reverse
|
/>
|
||||||
@click="toggleSubscription"
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="topics.length === 0"
|
||||||
|
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="`/images/states/${colorMode.value}/pack.svg`"
|
||||||
|
loading="lazy"
|
||||||
|
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
|
||||||
|
:alt="`${t('empty.subscription')}`"
|
||||||
|
/>
|
||||||
|
<span class="pb-4 text-center">
|
||||||
|
{{ t("empty.subscription") }}
|
||||||
|
</span>
|
||||||
|
<ButtonSecondary
|
||||||
|
:label="t('mqtt.new')"
|
||||||
|
filled
|
||||||
|
outline
|
||||||
|
@click="showSubscriptionModal(true)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<div
|
||||||
|
v-for="(topic, index) in topics"
|
||||||
|
:key="`subscription-${index}`"
|
||||||
|
class="flex flex-col"
|
||||||
|
>
|
||||||
|
<div class="flex items-stretch group">
|
||||||
|
<span class="flex items-center justify-center px-4 cursor-pointer">
|
||||||
|
<icon-lucide-rss
|
||||||
|
:style="{
|
||||||
|
color: topic.color,
|
||||||
|
}"
|
||||||
|
class="w-4 h-4 svg-icons"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="flex flex-1 min-w-0 py-2 pr-2 transition cursor-pointer group-hover:text-secondaryDark"
|
||||||
|
@click="openTopicAsTab(topic)"
|
||||||
|
>
|
||||||
|
<span class="truncate">
|
||||||
|
{{ topic.name }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<ButtonSecondary
|
||||||
|
v-tippy="{ theme: 'tooltip' }"
|
||||||
|
:icon="IconTrash"
|
||||||
|
color="red"
|
||||||
|
:title="t('mqtt.unsubscribe')"
|
||||||
|
class="hidden group-hover:inline-flex"
|
||||||
|
data-testid="unsubscribe_mqtt_subscription"
|
||||||
|
@click="unsubscribeFromTopic(topic.name)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RealtimeSubscription
|
||||||
|
:show="subscriptionModalShown"
|
||||||
|
:loading-state="subscribing"
|
||||||
|
@submit="subscribeToTopic"
|
||||||
|
@hide-modal="showSubscriptionModal(false)"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</AppPaneLayout>
|
</AppPaneLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import IconPlus from "~icons/lucide/plus"
|
||||||
|
import IconTrash from "~icons/lucide/trash"
|
||||||
|
import IconHelpCircle from "~icons/lucide/help-circle"
|
||||||
import { computed, onMounted, onUnmounted, ref, watch } from "vue"
|
import { computed, onMounted, onUnmounted, ref, watch } from "vue"
|
||||||
import { debounce } from "lodash-es"
|
import debounce from "lodash-es/debounce"
|
||||||
import { MQTTConnection, MQTTError } from "~/helpers/realtime/MQTTConnection"
|
import {
|
||||||
import { useI18n } from "@composables/i18n"
|
MQTTConnection,
|
||||||
import { useToast } from "@composables/toast"
|
MQTTConnectionConfig,
|
||||||
|
MQTTError,
|
||||||
|
MQTTTopic,
|
||||||
|
} from "~/helpers/realtime/MQTTConnection"
|
||||||
|
import { HoppRealtimeLogLine } from "~/helpers/types/HoppRealtimeLog"
|
||||||
|
import { useColorMode } from "@composables/theming"
|
||||||
import {
|
import {
|
||||||
useReadonlyStream,
|
useReadonlyStream,
|
||||||
useStream,
|
useStream,
|
||||||
useStreamSubscriber,
|
useStreamSubscriber,
|
||||||
} from "@composables/stream"
|
} from "@composables/stream"
|
||||||
|
import { useI18n } from "@composables/i18n"
|
||||||
|
import { useToast } from "@composables/toast"
|
||||||
import {
|
import {
|
||||||
addMQTTLogLine,
|
addMQTTLogLine,
|
||||||
MQTTConn$,
|
MQTTConn$,
|
||||||
MQTTEndpoint$,
|
MQTTEndpoint$,
|
||||||
|
MQTTClientID$,
|
||||||
MQTTLog$,
|
MQTTLog$,
|
||||||
setMQTTConn,
|
setMQTTConn,
|
||||||
setMQTTEndpoint,
|
setMQTTEndpoint,
|
||||||
|
setMQTTClientID,
|
||||||
setMQTTLog,
|
setMQTTLog,
|
||||||
|
MQTTTabs$,
|
||||||
|
setMQTTTabs,
|
||||||
|
MQTTCurrentTab$,
|
||||||
|
setCurrentTab,
|
||||||
|
addMQTTCurrentTabLogLine,
|
||||||
} from "~/newstore/MQTTSession"
|
} from "~/newstore/MQTTSession"
|
||||||
import RegexWorker from "@workers/regex?worker"
|
import RegexWorker from "@workers/regex?worker"
|
||||||
|
import { LogEntryData } from "~/components/realtime/Log.vue"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
const { subscribeToStream } = useStreamSubscriber()
|
const colorMode = useColorMode()
|
||||||
|
|
||||||
|
const { subscribeToStream } = useStreamSubscriber()
|
||||||
const url = useStream(MQTTEndpoint$, "", setMQTTEndpoint)
|
const url = useStream(MQTTEndpoint$, "", setMQTTEndpoint)
|
||||||
const log = useStream(MQTTLog$, [], setMQTTLog)
|
const clientID = useStream(MQTTClientID$, "", setMQTTClientID)
|
||||||
|
const config = ref<MQTTConnectionConfig>({
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
keepAlive: "60",
|
||||||
|
cleanSession: true,
|
||||||
|
lwTopic: "",
|
||||||
|
lwMessage: "",
|
||||||
|
lwQos: 0,
|
||||||
|
lwRetain: false,
|
||||||
|
})
|
||||||
|
const logs = useStream(MQTTLog$, [], setMQTTLog)
|
||||||
const socket = useStream(MQTTConn$, new MQTTConnection(), setMQTTConn)
|
const socket = useStream(MQTTConn$, new MQTTConnection(), setMQTTConn)
|
||||||
const connectionState = useReadonlyStream(
|
const connectionState = useReadonlyStream(
|
||||||
socket.value.connectionState$,
|
socket.value.connectionState$,
|
||||||
@@ -170,26 +271,24 @@ const subscriptionState = useReadonlyStream(
|
|||||||
socket.value.subscriptionState$,
|
socket.value.subscriptionState$,
|
||||||
false
|
false
|
||||||
)
|
)
|
||||||
|
const subscribing = useReadonlyStream(socket.value.subscribing$, false)
|
||||||
const isUrlValid = ref(true)
|
const isUrlValid = ref(true)
|
||||||
const pubTopic = ref("")
|
|
||||||
const subTopic = ref("")
|
const subTopic = ref("")
|
||||||
const message = ref("")
|
|
||||||
const username = ref("")
|
|
||||||
const password = ref("")
|
|
||||||
|
|
||||||
let worker: Worker
|
let worker: Worker
|
||||||
|
const subscriptionModalShown = ref(false)
|
||||||
|
const canSubscribe = computed(() => connectionState.value === "CONNECTED")
|
||||||
|
const topics = useReadonlyStream(socket.value.subscribedTopics$, [])
|
||||||
|
|
||||||
const canPublish = computed(
|
const currentTabId = useStream(MQTTCurrentTab$, "", setCurrentTab)
|
||||||
() =>
|
const tabs = useStream(MQTTTabs$, [], setMQTTTabs)
|
||||||
pubTopic.value !== "" &&
|
|
||||||
message.value !== "" &&
|
|
||||||
connectionState.value === "CONNECTED"
|
|
||||||
)
|
|
||||||
const canSubscribe = computed(
|
|
||||||
() => subTopic.value !== "" && connectionState.value === "CONNECTED"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
const onChangeConfig = (e: MQTTConnectionConfig) => {
|
||||||
|
config.value = e
|
||||||
|
}
|
||||||
|
|
||||||
|
const showSubscriptionModal = (show: boolean) => {
|
||||||
|
subscriptionModalShown.value = show
|
||||||
|
}
|
||||||
const workerResponseHandler = ({
|
const workerResponseHandler = ({
|
||||||
data,
|
data,
|
||||||
}: {
|
}: {
|
||||||
@@ -197,15 +296,13 @@ const workerResponseHandler = ({
|
|||||||
}) => {
|
}) => {
|
||||||
if (data.url === url.value) isUrlValid.value = data.result
|
if (data.url === url.value) isUrlValid.value = data.result
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
worker = new RegexWorker()
|
worker = new RegexWorker()
|
||||||
worker.addEventListener("message", workerResponseHandler)
|
worker.addEventListener("message", workerResponseHandler)
|
||||||
|
|
||||||
subscribeToStream(socket.value.event$, (event) => {
|
subscribeToStream(socket.value.event$, (event) => {
|
||||||
switch (event?.type) {
|
switch (event?.type) {
|
||||||
case "CONNECTING":
|
case "CONNECTING":
|
||||||
log.value = [
|
logs.value = [
|
||||||
{
|
{
|
||||||
payload: `${t("state.connecting_to", { name: url.value })}`,
|
payload: `${t("state.connecting_to", { name: url.value })}`,
|
||||||
source: "info",
|
source: "info",
|
||||||
@@ -214,9 +311,8 @@ onMounted(() => {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
break
|
break
|
||||||
|
|
||||||
case "CONNECTED":
|
case "CONNECTED":
|
||||||
log.value = [
|
logs.value = [
|
||||||
{
|
{
|
||||||
payload: `${t("state.connected_to", { name: url.value })}`,
|
payload: `${t("state.connected_to", { name: url.value })}`,
|
||||||
source: "info",
|
source: "info",
|
||||||
@@ -226,35 +322,38 @@ onMounted(() => {
|
|||||||
]
|
]
|
||||||
toast.success(`${t("state.connected")}`)
|
toast.success(`${t("state.connected")}`)
|
||||||
break
|
break
|
||||||
|
|
||||||
case "MESSAGE_SENT":
|
case "MESSAGE_SENT":
|
||||||
addMQTTLogLine({
|
addLog(
|
||||||
|
{
|
||||||
prefix: `${event.message.topic}`,
|
prefix: `${event.message.topic}`,
|
||||||
payload: event.message.message,
|
payload: event.message.message,
|
||||||
source: "client",
|
source: "client",
|
||||||
ts: Date.now(),
|
ts: Date.now(),
|
||||||
})
|
},
|
||||||
|
event.message.topic
|
||||||
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
case "MESSAGE_RECEIVED":
|
case "MESSAGE_RECEIVED":
|
||||||
addMQTTLogLine({
|
addLog(
|
||||||
|
{
|
||||||
prefix: `${event.message.topic}`,
|
prefix: `${event.message.topic}`,
|
||||||
payload: event.message.message,
|
payload: event.message.message,
|
||||||
source: "server",
|
source: "server",
|
||||||
ts: event.time,
|
ts: event.time,
|
||||||
})
|
},
|
||||||
|
event.message.topic
|
||||||
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
case "SUBSCRIBED":
|
case "SUBSCRIBED":
|
||||||
|
showSubscriptionModal(false)
|
||||||
addMQTTLogLine({
|
addMQTTLogLine({
|
||||||
payload: subscriptionState.value
|
payload: subscriptionState.value
|
||||||
? `${t("state.subscribed_success", { topic: subTopic.value })}`
|
? `${t("state.subscribed_success", { topic: event.topic })}`
|
||||||
: `${t("state.unsubscribed_success", { topic: subTopic.value })}`,
|
: `${t("state.unsubscribed_success", { topic: event.topic })}`,
|
||||||
source: "server",
|
source: "server",
|
||||||
ts: event.time,
|
ts: event.time,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
|
||||||
case "SUBSCRIPTION_FAILED":
|
case "SUBSCRIPTION_FAILED":
|
||||||
addMQTTLogLine({
|
addMQTTLogLine({
|
||||||
payload: subscriptionState.value
|
payload: subscriptionState.value
|
||||||
@@ -264,7 +363,6 @@ onMounted(() => {
|
|||||||
ts: event.time,
|
ts: event.time,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
|
||||||
case "ERROR":
|
case "ERROR":
|
||||||
addMQTTLogLine({
|
addMQTTLogLine({
|
||||||
payload: getI18nError(event.error),
|
payload: getI18nError(event.error),
|
||||||
@@ -273,11 +371,10 @@ onMounted(() => {
|
|||||||
ts: event.time,
|
ts: event.time,
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
|
||||||
case "DISCONNECTED":
|
case "DISCONNECTED":
|
||||||
addMQTTLogLine({
|
addMQTTLogLine({
|
||||||
payload: t("state.disconnected_from", { name: url.value }).toString(),
|
payload: t("state.disconnected_from", { name: url.value }).toString(),
|
||||||
source: "info",
|
source: "disconnected",
|
||||||
color: "#ff5555",
|
color: "#ff5555",
|
||||||
ts: event.time,
|
ts: event.time,
|
||||||
})
|
})
|
||||||
@@ -286,42 +383,48 @@ onMounted(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
const addLog = (line: HoppRealtimeLogLine, topic: string | undefined) => {
|
||||||
|
if (topic) addMQTTCurrentTabLogLine(topic, line)
|
||||||
|
addMQTTLogLine(line)
|
||||||
|
}
|
||||||
const debouncer = debounce(function () {
|
const debouncer = debounce(function () {
|
||||||
worker.postMessage({ type: "ws", url: url.value })
|
worker.postMessage({ type: "ws", url: url.value })
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
|
||||||
watch(url, (newUrl) => {
|
watch(url, (newUrl) => {
|
||||||
if (newUrl) debouncer()
|
if (newUrl) debouncer()
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
worker.terminate()
|
worker.terminate()
|
||||||
})
|
})
|
||||||
|
|
||||||
// METHODS
|
// METHODS
|
||||||
const toggleConnection = () => {
|
const toggleConnection = () => {
|
||||||
// If it is connecting:
|
// If it is connecting:
|
||||||
if (connectionState.value === "DISCONNECTED") {
|
if (connectionState.value === "DISCONNECTED") {
|
||||||
return socket.value.connect(url.value, username.value, password.value)
|
return socket.value.connect(url.value, clientID.value, config.value)
|
||||||
}
|
}
|
||||||
// Otherwise, it's disconnecting.
|
// Otherwise, it's disconnecting.
|
||||||
socket.value.disconnect()
|
socket.value.disconnect()
|
||||||
}
|
}
|
||||||
const publish = () => {
|
const publish = (event: { message: string; eventName: string }) => {
|
||||||
socket.value?.publish(pubTopic.value, message.value)
|
socket.value?.publish(event.eventName, event.message)
|
||||||
}
|
}
|
||||||
const toggleSubscription = () => {
|
const subscribeToTopic = (topic: MQTTTopic) => {
|
||||||
if (subscriptionState.value) {
|
if (canSubscribe.value) {
|
||||||
socket.value.unsubscribe(subTopic.value)
|
if (topics.value.some((t) => t.name === topic.name)) {
|
||||||
|
return toast.error(t("mqtt.already_subscribed").toString())
|
||||||
|
}
|
||||||
|
socket.value.subscribe(topic)
|
||||||
} else {
|
} else {
|
||||||
socket.value.subscribe(subTopic.value)
|
subscriptionModalShown.value = false
|
||||||
|
toast.error(t("mqtt.not_connected").toString())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const unsubscribeFromTopic = (topic: string) => {
|
||||||
|
socket.value.unsubscribe(topic)
|
||||||
|
removeTab(topic)
|
||||||
|
}
|
||||||
const getI18nError = (error: MQTTError): string => {
|
const getI18nError = (error: MQTTError): string => {
|
||||||
if (typeof error === "string") return error
|
if (typeof error === "string") return error
|
||||||
|
|
||||||
switch (error.type) {
|
switch (error.type) {
|
||||||
case "CONNECTION_NOT_ESTABLISHED":
|
case "CONNECTION_NOT_ESTABLISHED":
|
||||||
return t("state.connection_lost").toString()
|
return t("state.connection_lost").toString()
|
||||||
@@ -340,6 +443,34 @@ const getI18nError = (error: MQTTError): string => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const clearLogEntries = () => {
|
const clearLogEntries = () => {
|
||||||
log.value = []
|
logs.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
const openTopicAsTab = (topic: MQTTTopic) => {
|
||||||
|
const { name, color } = topic
|
||||||
|
if (tabs.value.some((tab) => tab.id === topic.name)) {
|
||||||
|
return (currentTabId.value = topic.name)
|
||||||
|
}
|
||||||
|
tabs.value = [
|
||||||
|
...tabs.value,
|
||||||
|
{
|
||||||
|
id: name,
|
||||||
|
name,
|
||||||
|
color,
|
||||||
|
removable: true,
|
||||||
|
logs: [],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
currentTabId.value = name
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortTabs = (e: { oldIndex: number; newIndex: number }) => {
|
||||||
|
const newTabs = [...tabs.value]
|
||||||
|
newTabs.splice(e.newIndex, 0, newTabs.splice(e.oldIndex, 1)[0])
|
||||||
|
tabs.value = newTabs
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeTab = (tabID: string) => {
|
||||||
|
tabs.value = tabs.value.filter((tab) => tab.id !== tabID)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -40,7 +40,7 @@
|
|||||||
:label="`Client ${version}`"
|
:label="`Client ${version}`"
|
||||||
@click="
|
@click="
|
||||||
() => {
|
() => {
|
||||||
onSelectVersion(version)
|
onSelectVersion(version as SIOClientVersion)
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@@ -107,6 +107,8 @@
|
|||||||
<RealtimeCommunication
|
<RealtimeCommunication
|
||||||
:show-event-field="true"
|
:show-event-field="true"
|
||||||
:is-connected="connectionState === 'CONNECTED'"
|
:is-connected="connectionState === 'CONNECTED'"
|
||||||
|
event-field-styles="top-upperSecondaryStickyFold"
|
||||||
|
sticky-header-styles="top-upperTertiaryStickyFold"
|
||||||
@send-message="sendMessage($event)"
|
@send-message="sendMessage($event)"
|
||||||
/>
|
/>
|
||||||
</SmartTab>
|
</SmartTab>
|
||||||
@@ -242,7 +244,7 @@
|
|||||||
<template #secondary>
|
<template #secondary>
|
||||||
<RealtimeLog
|
<RealtimeLog
|
||||||
:title="t('socketio.log')"
|
:title="t('socketio.log')"
|
||||||
:log="log"
|
:log="(log as LogEntryData[])"
|
||||||
@delete="clearLogEntries()"
|
@delete="clearLogEntries()"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -286,6 +288,7 @@ import {
|
|||||||
} from "~/newstore/SocketIOSession"
|
} from "~/newstore/SocketIOSession"
|
||||||
import { useColorMode } from "@composables/theming"
|
import { useColorMode } from "@composables/theming"
|
||||||
import RegexWorker from "@workers/regex?worker"
|
import RegexWorker from "@workers/regex?worker"
|
||||||
|
import { LogEntryData } from "~/components/realtime/Log.vue"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const colorMode = useColorMode()
|
const colorMode = useColorMode()
|
||||||
@@ -397,7 +400,7 @@ onMounted(() => {
|
|||||||
case "DISCONNECTED":
|
case "DISCONNECTED":
|
||||||
addSIOLogLine({
|
addSIOLogLine({
|
||||||
payload: t("state.disconnected_from", { name: url.value }).toString(),
|
payload: t("state.disconnected_from", { name: url.value }).toString(),
|
||||||
source: "info",
|
source: "disconnected",
|
||||||
color: "#ff5555",
|
color: "#ff5555",
|
||||||
ts: event.time,
|
ts: event.time,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
<template #secondary>
|
<template #secondary>
|
||||||
<RealtimeLog
|
<RealtimeLog
|
||||||
:title="t('sse.log')"
|
:title="t('sse.log')"
|
||||||
:log="log"
|
:log="(log as LogEntryData[])"
|
||||||
@delete="clearLogEntries()"
|
@delete="clearLogEntries()"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -88,6 +88,7 @@ import {
|
|||||||
} from "@composables/stream"
|
} from "@composables/stream"
|
||||||
import { SSEConnection } from "@helpers/realtime/SSEConnection"
|
import { SSEConnection } from "@helpers/realtime/SSEConnection"
|
||||||
import RegexWorker from "@workers/regex?worker"
|
import RegexWorker from "@workers/regex?worker"
|
||||||
|
import { LogEntryData } from "~/components/realtime/Log.vue"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -170,7 +171,7 @@ onMounted(() => {
|
|||||||
payload: t("state.disconnected_from", {
|
payload: t("state.disconnected_from", {
|
||||||
name: server.value,
|
name: server.value,
|
||||||
}).toString(),
|
}).toString(),
|
||||||
source: "info",
|
source: "disconnected",
|
||||||
color: "#ff5555",
|
color: "#ff5555",
|
||||||
ts: event.time,
|
ts: event.time,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
>
|
>
|
||||||
<RealtimeCommunication
|
<RealtimeCommunication
|
||||||
:is-connected="connectionState === 'CONNECTED'"
|
:is-connected="connectionState === 'CONNECTED'"
|
||||||
|
sticky-header-styles="top-upperSecondaryStickyFold"
|
||||||
@send-message="sendMessage($event)"
|
@send-message="sendMessage($event)"
|
||||||
/>
|
/>
|
||||||
</SmartTab>
|
</SmartTab>
|
||||||
@@ -178,7 +179,7 @@
|
|||||||
<template #secondary>
|
<template #secondary>
|
||||||
<RealtimeLog
|
<RealtimeLog
|
||||||
:title="t('websocket.log')"
|
:title="t('websocket.log')"
|
||||||
:log="log"
|
:log="(log as LogEntryData[])"
|
||||||
@delete="clearLogEntries()"
|
@delete="clearLogEntries()"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
@@ -220,6 +221,7 @@ import { useToast } from "@composables/toast"
|
|||||||
import { useColorMode } from "@composables/theming"
|
import { useColorMode } from "@composables/theming"
|
||||||
import { WSConnection, WSErrorMessage } from "@helpers/realtime/WSConnection"
|
import { WSConnection, WSErrorMessage } from "@helpers/realtime/WSConnection"
|
||||||
import RegexWorker from "@workers/regex?worker"
|
import RegexWorker from "@workers/regex?worker"
|
||||||
|
import { LogEntryData } from "~/components/realtime/Log.vue"
|
||||||
|
|
||||||
const t = useI18n()
|
const t = useI18n()
|
||||||
const toast = useToast()
|
const toast = useToast()
|
||||||
@@ -349,7 +351,7 @@ onMounted(() => {
|
|||||||
case "DISCONNECTED":
|
case "DISCONNECTED":
|
||||||
addWSLogLine({
|
addWSLogLine({
|
||||||
payload: t("state.disconnected_from", { name: url.value }).toString(),
|
payload: t("state.disconnected_from", { name: url.value }).toString(),
|
||||||
source: "info",
|
source: "disconnected",
|
||||||
color: "#ff5555",
|
color: "#ff5555",
|
||||||
ts: event.time,
|
ts: event.time,
|
||||||
})
|
})
|
||||||
|
|||||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -6670,7 +6670,7 @@ packages:
|
|||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
/escape-string-regexp/1.0.5:
|
/escape-string-regexp/1.0.5:
|
||||||
resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
|
resolution: {integrity: sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=}
|
||||||
engines: {node: '>=0.8.0'}
|
engines: {node: '>=0.8.0'}
|
||||||
|
|
||||||
/escape-string-regexp/2.0.0:
|
/escape-string-regexp/2.0.0:
|
||||||
@@ -7918,7 +7918,7 @@ packages:
|
|||||||
wrappy: 1.0.2
|
wrappy: 1.0.2
|
||||||
|
|
||||||
/inherits/2.0.3:
|
/inherits/2.0.3:
|
||||||
resolution: {integrity: sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==}
|
resolution: {integrity: sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/inherits/2.0.4:
|
/inherits/2.0.4:
|
||||||
@@ -9366,7 +9366,7 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/memory-fs/0.3.0:
|
/memory-fs/0.3.0:
|
||||||
resolution: {integrity: sha512-QTNXnl79X97kZ9jJk/meJrtDuvgvRakX5LU7HZW1L7MsXHuSTwoMIzN9tOLLH3Xfsj/gbsSqX/ovnsqz246zKQ==}
|
resolution: {integrity: sha1-e8xrYp46Q+hx1+Kaymrop/FcuyA=}
|
||||||
dependencies:
|
dependencies:
|
||||||
errno: 0.1.8
|
errno: 0.1.8
|
||||||
readable-stream: 2.3.7
|
readable-stream: 2.3.7
|
||||||
@@ -9537,7 +9537,7 @@ packages:
|
|||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/ms/2.0.0:
|
/ms/2.0.0:
|
||||||
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
resolution: {integrity: sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/ms/2.1.2:
|
/ms/2.1.2:
|
||||||
@@ -10191,7 +10191,7 @@ packages:
|
|||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/prr/1.0.1:
|
/prr/1.0.1:
|
||||||
resolution: {integrity: sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==}
|
resolution: {integrity: sha1-0/wRS6BplaRexok/SEzrHXj19HY=}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/psl/1.9.0:
|
/psl/1.9.0:
|
||||||
|
|||||||
Reference in New Issue
Block a user