fix(mqtt): persist topics and support advanced options

This commit is contained in:
jinmo authored and GitHub committed 2026-08-06 23:15:17 +08:00
1 parent 670ddcfcb3
commit ab586d121a
19 files changed
+347 -51

No files matched your search

@@ -710,14 +710,18 @@ const mqttClientId = ref("");
const mqttProtocolVersion = ref<"v3" | "v4" | "v5">("v5");
const mqttTransportMode = ref<"tcp" | "websocket">("tcp");
const mqttWsPath = ref("/mqtt");
const mqttAuthKind = ref<"none" | "password">("none");
const mqttAuthKind = ref<"none" | "password" | "certificate">("none");
const mqttUsername = ref("");
const mqttPassword = ref("");
const mqttCaCertPath = ref("");
const mqttClientCertPath = ref("");
const mqttClientKeyPath = ref("");
const mqttTls = ref(false);
const mqttTlsSkipVerify = ref(false);
const mqttKeepAliveSecs = ref(60);
const mqttConnectTimeoutSecs = ref(30);
const mqttMaxPacketSizeBytes = ref(16 * 1024 * 1024);
const mqttSavedTopics = ref<MqttConnectionConfig["savedTopics"]>([]);
const nacosPrimaryAddressPlaceholder = computed(() => {
return "http://127.0.0.1:8848/nacos";
});
@@ -1231,15 +1235,29 @@ function resetMqttFields(config?: Partial<MqttConnectionConfig>) {
mqttKeepAliveSecs.value = Math.max(1, config?.keepAliveSecs || 60);
mqttConnectTimeoutSecs.value = Math.max(1, config?.connectTimeoutSecs || 30);
mqttMaxPacketSizeBytes.value = Math.min(268435455, Math.max(1024, config?.maxPacketSizeBytes || 16 * 1024 * 1024));
mqttSavedTopics.value = config?.savedTopics ?? [];
const auth = config?.auth;
if (auth && auth.kind === "password") {
mqttAuthKind.value = "password";
mqttUsername.value = auth.username || "";
mqttPassword.value = auth.password || "";
mqttCaCertPath.value = "";
mqttClientCertPath.value = "";
mqttClientKeyPath.value = "";
} else if (auth && auth.kind === "certificate") {
mqttAuthKind.value = "certificate";
mqttUsername.value = "";
mqttPassword.value = "";
mqttCaCertPath.value = auth.caCertPath || "";
mqttClientCertPath.value = auth.clientCertPath || "";
mqttClientKeyPath.value = auth.clientKeyPath || "";
} else {
mqttAuthKind.value = "none";
mqttUsername.value = "";
mqttPassword.value = "";
mqttCaCertPath.value = "";
mqttClientCertPath.value = "";
mqttClientKeyPath.value = "";
}
}
@@ -1252,7 +1270,12 @@ function hydrateMqttFields(value: unknown) {
}
function buildMqttExternalConfig(): MqttConnectionConfig {
const auth: MqttConnectionConfig["auth"] = mqttAuthKind.value === "password" ? { kind: "password", username: mqttUsername.value, password: mqttPassword.value } : { kind: "none" };
const auth: MqttConnectionConfig["auth"] =
mqttAuthKind.value === "password"
? { kind: "password", username: mqttUsername.value, password: mqttPassword.value }
: mqttAuthKind.value === "certificate"
? { kind: "certificate", caCertPath: mqttCaCertPath.value || undefined, clientCertPath: mqttClientCertPath.value || undefined, clientKeyPath: mqttClientKeyPath.value || undefined }
: { kind: "none" };
return {
host: mqttHost.value.trim(),
@@ -1266,6 +1289,7 @@ function buildMqttExternalConfig(): MqttConnectionConfig {
keepAliveSecs: Math.max(1, mqttKeepAliveSecs.value),
connectTimeoutSecs: Math.max(1, mqttConnectTimeoutSecs.value),
maxPacketSizeBytes: Math.min(268435455, Math.max(1024, mqttMaxPacketSizeBytes.value)),
savedTopics: mqttSavedTopics.value,
wsPath: mqttTransportMode.value === "websocket" ? mqttWsPath.value || "/mqtt" : undefined,
};
}
@@ -6050,6 +6074,7 @@ function openExternalUrl(url: string) {
<div class="col-span-3 flex gap-2">
<Button size="sm" :variant="mqttAuthKind === 'none' ? 'default' : 'outline'" @click="mqttAuthKind = 'none'">{{ t("connection.mqAuthNone") }}</Button>
<Button size="sm" :variant="mqttAuthKind === 'password' ? 'default' : 'outline'" @click="mqttAuthKind = 'password'">{{ t("connection.mqAuthBasic") }}</Button>
<Button size="sm" :variant="mqttAuthKind === 'certificate' ? 'default' : 'outline'" @click="mqttAuthKind = 'certificate'">{{ t("connection.mqttAuthCertificate") }}</Button>
</div>
</div>
<template v-if="mqttAuthKind === 'password'">
@@ -6062,6 +6087,20 @@ function openExternalUrl(url: string) {
<Input v-model="mqttPassword" type="password" class="col-span-3" :placeholder="t('connection.mqttPasswordPlaceholder')" />
</div>
</template>
<template v-else-if="mqttAuthKind === 'certificate'">
<div class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelClass">{{ t("connection.mqttCaCertPath") }}</Label>
<Input v-model="mqttCaCertPath" class="col-span-3" placeholder="/path/to/ca.pem" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelClass">{{ t("connection.mqttClientCertPath") }}</Label>
<Input v-model="mqttClientCertPath" class="col-span-3" placeholder="/path/to/client.crt" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelClass">{{ t("connection.mqttClientKeyPath") }}</Label>
<Input v-model="mqttClientKeyPath" class="col-span-3" placeholder="/path/to/client.key" />
</div>
</template>
<div class="grid grid-cols-4 items-center gap-4">
<Label :class="connectionLabelClass">{{ t("connection.mqttTls") }}</Label>
<div class="col-span-3 flex items-center gap-2">
@@ -20,6 +20,7 @@ const brokerInfo = ref<MqttBrokerInfo | null>(null);
const topicTree = ref<MqttTopicNode | null>(null);
const messages = ref<MqttMessage[]>([]);
const subscribedTopics = ref<[string, string][]>([]);
const noLocalSubscribe = ref(false);
const selectedTopic = ref<string>(props.initialTopic ?? "");
const loading = ref(true);
const error = ref<string | null>(null);
@@ -52,9 +53,9 @@ async function refreshData() {
}
}
async function handleSubscribe(topic: string) {
async function handleSubscribe(topic: string, noLocal = noLocalSubscribe.value) {
try {
await mqttSubscribe(props.connectionId, topic, "atmostonce");
await mqttSubscribe(props.connectionId, topic, "atmostonce", noLocal);
await refreshData();
} catch (e) {
error.value = String(e);
@@ -194,6 +195,7 @@ onUnmounted(() => {
"
>
<input class="flex-1 h-7 px-2 text-xs rounded border bg-transparent" :placeholder="t('connection.mqttSubscribePlaceholder')" />
<label class="flex items-center gap-1 text-[10px] whitespace-nowrap"><input v-model="noLocalSubscribe" type="checkbox" />{{ t("connection.mqttNoLocal") }}</label>
<Button size="sm" variant="ghost" class="h-7 text-xs" type="submit">{{ t("connection.mqttSubscribe") }}</Button>
</form>
</div>
@@ -216,13 +218,13 @@ onUnmounted(() => {
</select>
</div>
</div>
<div class="flex-1 overflow-auto">
<div class="flex-1 overflow-auto flex flex-col">
<div v-if="messages.length === 0" class="text-xs text-muted-foreground p-4 text-center">{{ t("connection.mqttNoMessages") }}</div>
<div
v-for="(msg, i) in messages"
:key="i"
class="px-3 py-2 border-b last:border-b-0 cursor-pointer text-xs"
:class="msg.direction === 'sent' ? 'bg-emerald-50/60 dark:bg-emerald-950/20 hover:bg-emerald-100/60 dark:hover:bg-emerald-950/40' : 'hover:bg-muted/50'"
class="px-3 py-2 border-b last:border-b-0 cursor-pointer text-xs self-start w-full"
:class="msg.direction === 'sent' ? 'bg-emerald-50/60 dark:bg-emerald-950/20 hover:bg-emerald-100/60 dark:hover:bg-emerald-950/40 ml-auto max-w-[85%] rounded-l-md' : 'hover:bg-muted/50 mr-auto max-w-[85%] rounded-r-md'"
@click="handleTopicClick(msg.topic)"
>
<div class="flex items-center gap-2 mb-0.5">
@@ -32,7 +32,7 @@ const qosHints = computed(() => [t("connection.mqttQosAtMostOnce"), t("connectio
const payloadPlaceholder = computed(() => {
switch (encoding.value) {
case "json":
return t("connection.mqttPayloadPlaceholderJson");
return t("connection.mqttPayloadPlaceholderJson", { example: '{"key": "value"}' });
case "base64":
return t("connection.mqttPayloadPlaceholderBase64");
case "hex":
+6 -1
View File
@@ -526,6 +526,10 @@ export default {
mqttUsername: "Username",
mqttUsernamePlaceholder: "Optional",
mqttPassword: "Password",
mqttAuthCertificate: "Client certificate",
mqttCaCertPath: "CA certificate path",
mqttClientCertPath: "Client certificate path",
mqttClientKeyPath: "Client private key path",
mqttPasswordPlaceholder: "Optional",
mqttTls: "TLS",
mqttTlsSkipVerify: "Skip certificate verification",
@@ -542,6 +546,7 @@ export default {
mqttMessages: "Messages",
mqttPublish: "Publish Message",
mqttSubscribe: "Subscribe",
mqttNoLocal: "No Local",
mqttUnsubscribe: "Unsubscribe",
mqttNoMessages: "No messages",
mqttNoTopics: "No subscribed topics",
@@ -571,7 +576,7 @@ export default {
mqttPayloadEncoding: "Payload encoding",
mqttPayload: "Payload",
mqttPayloadPlaceholderPlaintext: "Enter message content...",
mqttPayloadPlaceholderJson: 'Enter JSON, for example {"key": "value"}',
mqttPayloadPlaceholderJson: "Enter JSON, for example {example}",
mqttPayloadPlaceholderBase64: "Enter Base64-encoded data",
mqttPayloadPlaceholderHex: "Enter hexadecimal data, for example 48 65 6C 6C 6F",
mqttPayloadPlaceholderStructured: "Enter a JSON value; it will be encoded as {encoding}",
+6 -1
View File
@@ -792,7 +792,7 @@ export default withEnglishFallback({
mqttPayloadEncoding: "Codificación del Payload",
mqttPayload: "Payload",
mqttPayloadPlaceholderPlaintext: "Introduce el contenido del mensaje...",
mqttPayloadPlaceholderJson: 'Introduce JSON, por ejemplo {"key": "value"}',
mqttPayloadPlaceholderJson: "Introduce JSON, por ejemplo {example}",
mqttPayloadPlaceholderBase64: "Introduce datos codificados en Base64",
mqttPayloadPlaceholderHex: "Introduce datos hexadecimales, por ejemplo 48 65 6C 6C 6F",
mqttPayloadPlaceholderStructured: "Introduce un valor JSON; se codificará como {encoding}",
@@ -812,6 +812,11 @@ export default withEnglishFallback({
zookeeperAuthMethod: "Método de autenticación",
zookeeperAuthDigest: "Digest (predeterminado)",
zookeeperAuthSaslDigest: "SASL DIGEST-MD5",
mqttAuthCertificate: "Certificado de cliente",
mqttCaCertPath: "Ruta del certificado CA",
mqttClientCertPath: "Ruta del certificado de cliente",
mqttClientKeyPath: "Ruta de la clave privada del cliente",
mqttNoLocal: "Prohibir reenvío local",
},
editor: {
duckdbDraining: "La consulta anterior de DuckDB aún se está deteniendo. Vuelva a intentarlo en breve.",
+6 -1
View File
@@ -790,7 +790,7 @@ export default withEnglishFallback({
mqttPayloadEncoding: "Codifica del Payload",
mqttPayload: "Payload",
mqttPayloadPlaceholderPlaintext: "Inserisci il contenuto del messaggio...",
mqttPayloadPlaceholderJson: 'Inserisci JSON, ad esempio {"key": "value"}',
mqttPayloadPlaceholderJson: "Inserisci JSON, ad esempio {example}",
mqttPayloadPlaceholderBase64: "Inserisci dati codificati in Base64",
mqttPayloadPlaceholderHex: "Inserisci dati esadecimali, ad esempio 48 65 6C 6C 6F",
mqttPayloadPlaceholderStructured: "Inserisci un valore JSON; verrà codificato come {encoding}",
@@ -810,6 +810,11 @@ export default withEnglishFallback({
zookeeperAuthMethod: "Metodo di autenticazione",
zookeeperAuthDigest: "Digest (predefinito)",
zookeeperAuthSaslDigest: "SASL DIGEST-MD5",
mqttAuthCertificate: "Certificato client",
mqttCaCertPath: "Percorso del certificato CA",
mqttClientCertPath: "Percorso del certificato client",
mqttClientKeyPath: "Percorso della chiave privata client",
mqttNoLocal: "Disabilita inoltro locale",
},
editor: {
duckdbDraining: "La query DuckDB precedente è ancora in fase di arresto. Riprova a breve.",
+6 -1
View File
@@ -790,7 +790,7 @@ export default withEnglishFallback({
mqttPayloadEncoding: "ペイロードのエンコード",
mqttPayload: "ペイロード",
mqttPayloadPlaceholderPlaintext: "メッセージ内容を入力...",
mqttPayloadPlaceholderJson: 'JSON を入力(例: {"key": "value"})',
mqttPayloadPlaceholderJson: "JSON を入力(例: {example})",
mqttPayloadPlaceholderBase64: "Base64 エンコード済みデータを入力",
mqttPayloadPlaceholderHex: "16 進数データを入力(例: 48 65 6C 6C 6F)",
mqttPayloadPlaceholderStructured: "JSON 値を入力してください。{encoding} としてエンコードされます",
@@ -831,6 +831,11 @@ export default withEnglishFallback({
zookeeperAuthMethod: "認証方式",
zookeeperAuthDigest: "Digest(デフォルト)",
zookeeperAuthSaslDigest: "SASL DIGEST-MD5",
mqttAuthCertificate: "クライアント証明書",
mqttCaCertPath: "CA証明書パス",
mqttClientCertPath: "クライアント証明書パス",
mqttClientKeyPath: "クライアント秘密鍵パス",
mqttNoLocal: "ローカル転送禁止",
},
editor: {
duckdbDraining: "前回の DuckDB クエリはまだ停止処理中です。しばらくしてから再試行してください。",
+6 -1
View File
@@ -791,7 +791,7 @@ export default withEnglishFallback({
mqttPayloadEncoding: "Codificação do Payload",
mqttPayload: "Payload",
mqttPayloadPlaceholderPlaintext: "Digite o conteúdo da mensagem...",
mqttPayloadPlaceholderJson: 'Digite JSON, por exemplo {"key": "value"}',
mqttPayloadPlaceholderJson: "Digite JSON, por exemplo {example}",
mqttPayloadPlaceholderBase64: "Digite dados codificados em Base64",
mqttPayloadPlaceholderHex: "Digite dados hexadecimais, por exemplo 48 65 6C 6C 6F",
mqttPayloadPlaceholderStructured: "Digite um valor JSON; ele será codificado como {encoding}",
@@ -811,6 +811,11 @@ export default withEnglishFallback({
zookeeperAuthMethod: "Método de autenticação",
zookeeperAuthDigest: "Digest (padrão)",
zookeeperAuthSaslDigest: "SASL DIGEST-MD5",
mqttAuthCertificate: "Certificado do cliente",
mqttCaCertPath: "Caminho do certificado CA",
mqttClientCertPath: "Caminho do certificado do cliente",
mqttClientKeyPath: "Caminho da chave privada do cliente",
mqttNoLocal: "Proibir encaminhamento local",
},
editor: {
duckdbDraining: "A consulta anterior do DuckDB ainda está sendo interrompida. Tente novamente em breve.",
+6 -1
View File
@@ -527,6 +527,10 @@ export default withEnglishFallback({
mqttUsername: "用户名",
mqttUsernamePlaceholder: "选填",
mqttPassword: "密码",
mqttAuthCertificate: "客户端证书",
mqttCaCertPath: "CA 证书路径",
mqttClientCertPath: "客户端证书路径",
mqttClientKeyPath: "客户端私钥路径",
mqttPasswordPlaceholder: "选填",
mqttTls: "TLS",
mqttTlsSkipVerify: "跳过证书校验",
@@ -543,6 +547,7 @@ export default withEnglishFallback({
mqttMessages: "消息",
mqttPublish: "发布消息",
mqttSubscribe: "订阅",
mqttNoLocal: "禁止本地转发",
mqttUnsubscribe: "取消订阅",
mqttNoMessages: "暂无消息",
mqttNoTopics: "暂无已订阅的 Topic",
@@ -572,7 +577,7 @@ export default withEnglishFallback({
mqttPayloadEncoding: "Payload 编码",
mqttPayload: "消息内容 (Payload)",
mqttPayloadPlaceholderPlaintext: "输入消息内容...",
mqttPayloadPlaceholderJson: '输入 JSON,例如 {"key": "value"}',
mqttPayloadPlaceholderJson: "输入 JSON,例如 {example}",
mqttPayloadPlaceholderBase64: "输入 Base64 编码的数据",
mqttPayloadPlaceholderHex: "输入十六进制数据,例如 48 65 6C 6C 6F",
mqttPayloadPlaceholderStructured: "输入 JSON 值,发送时将自动编码为 {encoding}",
+6 -1
View File
@@ -790,7 +790,7 @@ export default withEnglishFallback({
mqttPayloadEncoding: "Payload 編碼",
mqttPayload: "訊息內容 (Payload)",
mqttPayloadPlaceholderPlaintext: "輸入訊息內容...",
mqttPayloadPlaceholderJson: '輸入 JSON,例如 {"key": "value"}',
mqttPayloadPlaceholderJson: "輸入 JSON,例如 {example}",
mqttPayloadPlaceholderBase64: "輸入 Base64 編碼的資料",
mqttPayloadPlaceholderHex: "輸入十六進位資料,例如 48 65 6C 6C 6F",
mqttPayloadPlaceholderStructured: "輸入 JSON 值,傳送時將自動編碼為 {encoding}",
@@ -810,6 +810,11 @@ export default withEnglishFallback({
zookeeperAuthMethod: "認證方式",
zookeeperAuthDigest: "Digest(預設)",
zookeeperAuthSaslDigest: "SASL DIGEST-MD5",
mqttAuthCertificate: "用戶端憑證",
mqttCaCertPath: "CA 憑證路徑",
mqttClientCertPath: "用戶端憑證路徑",
mqttClientKeyPath: "用戶端私鑰路徑",
mqttNoLocal: "禁止本機轉送",
},
editor: {
duckdbDraining: "上一筆 DuckDB 查詢仍在停止,請稍後重試。",
+1 -1
View File
@@ -9,7 +9,7 @@ export async function mqttGetBrokerInfo(_connectionId: string) {
mqttWebNotAvailable("mqttGetBrokerInfo");
}
export async function mqttSubscribe(_connectionId: string, _topic: string, _qos?: string | null) {
export async function mqttSubscribe(_connectionId: string, _topic: string, _qos?: string | null, _noLocal?: boolean) {
mqttWebNotAvailable("mqttSubscribe");
}
+2 -2
View File
@@ -4,8 +4,8 @@ export async function mqttGetBrokerInfo(connectionId: string) {
return invoke("mqtt_get_broker_info", { connectionId });
}
export async function mqttSubscribe(connectionId: string, topic: string, qos?: string | null) {
return invoke("mqtt_subscribe", { connectionId, topic, qos: qos ?? null });
export async function mqttSubscribe(connectionId: string, topic: string, qos?: string | null, noLocal?: boolean) {
return invoke("mqtt_subscribe", { connectionId, topic, qos: qos ?? null, noLocal: noLocal ?? false });
}
export async function mqttUnsubscribe(connectionId: string, topic: string) {
+7
View File
@@ -26,6 +26,13 @@ export interface MqttConnectionConfig {
connectTimeoutSecs: number;
maxPacketSizeBytes: number;
wsPath?: string;
savedTopics?: MqttSavedTopic[];
}
export interface MqttSavedTopic {
topic: string;
qos: MqttQoS;
noLocal?: boolean;
}
export interface MqttBrokerInfo {