Compare commits

...
Author SHA1 Message Date
copilot-swe-agent[bot]andthomasnordquist be83c3f2cc Replace arrow buttons with drag-and-drop reordering using drag handles
Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com>
2025-12-23 07:55:56 +00:00
copilot-swe-agent[bot]andthomasnordquist 1a8a9224b4 Address code review feedback: use deterministic ordering and proper styling
Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com>
2025-12-22 19:32:04 +00:00
copilot-swe-agent[bot]andthomasnordquist 651246ff83 Add connection ordering functionality with tests
Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com>
2025-12-22 19:18:52 +00:00
copilot-swe-agent[bot] 73b8e6ff9f Initial plan 2025-12-22 19:09:06 +00:00
Copilot eb605a884c Fix Playwright strict mode violation in demo video clipboard tests (#952) 2025-12-22 17:19:53 +01:00
dependabot[bot] 9868ac67fc chore(deps): bump @babel/runtime from 7.24.0 to 7.28.4 in /app (#950)
Bumps
[@babel/runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime)
from 7.24.0 to 7.28.4.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/babel/babel/releases"><code>@​babel/runtime</code>'s
releases</a>.</em></p>
<blockquote>
<h2>v7.28.4 (2025-09-05)</h2>
<p>Thanks <a
href="https://github.com/gwillen"><code>@​gwillen</code></a> and <a
href="https://github.com/mrginglymus"><code>@​mrginglymus</code></a> for
your first PRs!</p>
<h4>🏠 Internal</h4>
<ul>
<li><code>babel-core</code>,
<code>babel-helper-check-duplicate-nodes</code>,
<code>babel-traverse</code>, <code>babel-types</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17493">#17493</a>
Update Jest to v30.1.1 (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
<li><code>babel-plugin-transform-regenerator</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17455">#17455</a>
chore: Clean up <code>transform-regenerator</code> (<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
<li><code>babel-core</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17474">#17474</a>
Switch to <code>@​jridgewell/remapping</code> (<a
href="https://github.com/mrginglymus"><code>@​mrginglymus</code></a>)</li>
</ul>
</li>
</ul>
<h4>Committers: 5</h4>
<ul>
<li>Babel Bot (<a
href="https://github.com/babel-bot"><code>@​babel-bot</code></a>)</li>
<li>Bill Collins (<a
href="https://github.com/mrginglymus"><code>@​mrginglymus</code></a>)</li>
<li>Glenn Willen (<a
href="https://github.com/gwillen"><code>@​gwillen</code></a>)</li>
<li>Huáng Jùnliàng (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
<li><a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a></li>
</ul>
<h2>v7.28.3 (2025-08-14)</h2>
<h4>👓 Spec Compliance</h4>
<ul>
<li><code>babel-helper-create-class-features-plugin</code>,
<code>babel-plugin-proposal-decorators</code>,
<code>babel-plugin-transform-class-static-block</code>,
<code>babel-preset-env</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17443">#17443</a>
[static blocks] Do not inject new static fields after static code (<a
href="https://github.com/nicolo-ribaudo"><code>@​nicolo-ribaudo</code></a>)</li>
</ul>
</li>
</ul>
<h4>🐛 Bug Fix</h4>
<ul>
<li><code>babel-parser</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17465">#17465</a>
fix(parser/typescript): parse <code>import(&quot;./a&quot;,
{with:{},})</code> (<a
href="https://github.com/easrng"><code>@​easrng</code></a>)</li>
<li><a
href="https://redirect.github.com/babel/babel/pull/17478">#17478</a>
fix(parser): stop subscript parsing on async arrow (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
</ul>
<h4>💅 Polish</h4>
<ul>
<li><code>babel-plugin-transform-regenerator</code>,
<code>babel-plugin-transform-runtime</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17363">#17363</a> Do
not save last yield in call in temp var (<a
href="https://github.com/nicolo-ribaudo"><code>@​nicolo-ribaudo</code></a>)</li>
</ul>
</li>
</ul>
<h4>📝 Documentation</h4>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17448">#17448</a>
move eslint-{parser,plugin} docs to the website (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
<h4>🏠 Internal</h4>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17454">#17454</a>
Enable type checking for <code>scripts</code> and
<code>babel-worker.cjs</code> (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
<h4>🔬 Output optimization</h4>
<ul>
<li><code>babel-plugin-proposal-destructuring-private</code>,
<code>babel-plugin-proposal-do-expressions</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17444">#17444</a>
Optimize do expression output (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
</ul>
<h4>Committers: 5</h4>
<ul>
<li>Babel Bot (<a
href="https://github.com/babel-bot"><code>@​babel-bot</code></a>)</li>
<li>Huáng Jùnliàng (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
<li>Jam Balaya (<a
href="https://github.com/JamBalaya56562"><code>@​JamBalaya56562</code></a>)</li>
<li>Nicolò Ribaudo (<a
href="https://github.com/nicolo-ribaudo"><code>@​nicolo-ribaudo</code></a>)</li>
<li>easrng (<a
href="https://github.com/easrng"><code>@​easrng</code></a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/babel/babel/blob/main/CHANGELOG.md"><code>@​babel/runtime</code>'s
changelog</a>.</em></p>
<blockquote>
<h2>v7.28.4 (2025-09-05)</h2>
<h4>🏠 Internal</h4>
<ul>
<li><code>babel-core</code>,
<code>babel-helper-check-duplicate-nodes</code>,
<code>babel-traverse</code>, <code>babel-types</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17493">#17493</a>
Update Jest to v30.1.1 (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
<li><code>babel-plugin-transform-regenerator</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17455">#17455</a>
chore: Clean up <code>transform-regenerator</code> (<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
<li><code>babel-core</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17474">#17474</a>
Switch to <code>@​jridgewell/remapping</code> (<a
href="https://github.com/mrginglymus"><code>@​mrginglymus</code></a>)</li>
</ul>
</li>
</ul>
<h2>v7.28.3 (2025-08-14)</h2>
<h4>👓 Spec Compliance</h4>
<ul>
<li><code>babel-helper-create-class-features-plugin</code>,
<code>babel-plugin-proposal-decorators</code>,
<code>babel-plugin-transform-class-static-block</code>,
<code>babel-preset-env</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17443">#17443</a>
[static blocks] Do not inject new static fields after static code (<a
href="https://github.com/nicolo-ribaudo"><code>@​nicolo-ribaudo</code></a>)</li>
</ul>
</li>
</ul>
<h4>🐛 Bug Fix</h4>
<ul>
<li><code>babel-parser</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17465">#17465</a>
fix(parser/typescript): parse <code>import(&quot;./a&quot;,
{with:{},})</code> (<a
href="https://github.com/easrng"><code>@​easrng</code></a>)</li>
<li><a
href="https://redirect.github.com/babel/babel/pull/17478">#17478</a>
fix(parser): stop subscript parsing on async arrow (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
</ul>
<h4>💅 Polish</h4>
<ul>
<li><code>babel-plugin-transform-regenerator</code>,
<code>babel-plugin-transform-runtime</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17363">#17363</a> Do
not save last yield in call in temp var (<a
href="https://github.com/nicolo-ribaudo"><code>@​nicolo-ribaudo</code></a>)</li>
</ul>
</li>
</ul>
<h4>📝 Documentation</h4>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17448">#17448</a>
move eslint-{parser,plugin} docs to the website (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
<h4>🏠 Internal</h4>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17454">#17454</a>
Enable type checking for <code>scripts</code> and
<code>babel-worker.cjs</code> (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
<h4>🔬 Output optimization</h4>
<ul>
<li><code>babel-plugin-proposal-destructuring-private</code>,
<code>babel-plugin-proposal-do-expressions</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17444">#17444</a>
Optimize do expression output (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
</ul>
<h2>v7.28.2 (2025-07-24)</h2>
<h4>🐛 Bug Fix</h4>
<ul>
<li><code>babel-types</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17445">#17445</a>
[babel 7] Make <code>operator</code> param in
<code>t.tsTypeOperator</code> optional (<a
href="https://github.com/nicolo-ribaudo"><code>@​nicolo-ribaudo</code></a>)</li>
</ul>
</li>
<li><code>babel-helpers</code>,
<code>babel-plugin-transform-async-generator-functions</code>,
<code>babel-plugin-transform-regenerator</code>,
<code>babel-preset-env</code>, <code>babel-runtime-corejs3</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17441">#17441</a>
fix: <code>regeneratorDefine</code> compatibility with es5 strict mode
(<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
</ul>
<h2>v7.28.1 (2025-07-12)</h2>
<h4>🐛 Bug Fix</h4>
<ul>
<li><code>babel-plugin-transform-async-generator-functions</code>,
<code>babel-plugin-transform-regenerator</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17426">#17426</a>
fix: <code>regenerator</code> correctly handles <code>throw</code>
outside of <code>try</code> (<a
href="https://github.com/liuxingbaoyu"><code>@​liuxingbaoyu</code></a>)</li>
</ul>
</li>
</ul>
<h4>📝 Documentation</h4>
<ul>
<li><code>babel-types</code>
<ul>
<li><a
href="https://redirect.github.com/babel/babel/pull/17422">#17422</a> Add
missing FunctionParameter docs (<a
href="https://github.com/JLHwung"><code>@​JLHwung</code></a>)</li>
</ul>
</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/babel/babel/commit/35055e392079a65830b7bf5b1d1c1fc4de90a78f"><code>35055e3</code></a>
v7.28.4</li>
<li><a
href="https://github.com/babel/babel/commit/ef155f5ca83c73dbc1ea8d95216830b7dc3b0ac2"><code>ef155f5</code></a>
v7.28.3</li>
<li><a
href="https://github.com/babel/babel/commit/cac0ff4c3426eed30b4d27e7971b348da7c9f1e6"><code>cac0ff4</code></a>
v7.28.2</li>
<li><a
href="https://github.com/babel/babel/commit/f68ac511f091f6d1f698e8ce59cd668d3bfc6102"><code>f68ac51</code></a>
chore: Avoid CITGM errors (<a
href="https://github.com/babel/babel/tree/HEAD/packages/babel-runtime/issues/17382">#17382</a>)</li>
<li><a
href="https://github.com/babel/babel/commit/baa4cb8b9f8a551d7dae9042b19ea2f74df6b110"><code>baa4cb8</code></a>
v7.27.6</li>
<li><a
href="https://github.com/babel/babel/commit/7d069309fdfcedda2928a043f6f7c98135c1242a"><code>7d06930</code></a>
v7.27.4</li>
<li><a
href="https://github.com/babel/babel/commit/5b9468d9bf1ab4f427241673e9f03593da115a69"><code>5b9468d</code></a>
Reduce <code>regenerator</code> size more (<a
href="https://github.com/babel/babel/tree/HEAD/packages/babel-runtime/issues/17287">#17287</a>)</li>
<li><a
href="https://github.com/babel/babel/commit/cb78b5b50e327e27467086cf8bbe196bda7cea9b"><code>cb78b5b</code></a>
[babel 8] Do not replace global <code>regeneratorRuntime</code>
references in regenerato...</li>
<li><a
href="https://github.com/babel/babel/commit/a0690e39ea63cdcc3d9282ece739e6677c83ad6e"><code>a0690e3</code></a>
Split <code>regeneratorRuntime</code> into multiple helpers (<a
href="https://github.com/babel/babel/tree/HEAD/packages/babel-runtime/issues/17238">#17238</a>)</li>
<li><a
href="https://github.com/babel/babel/commit/da5e371efabf6c0baab1ec2c888da189e1b610ad"><code>da5e371</code></a>
v7.27.3</li>
<li>Additional commits viewable in <a
href="https://github.com/babel/babel/commits/v7.28.4/packages/babel-runtime">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=@babel/runtime&package-manager=npm_and_yarn&previous-version=7.24.0&new-version=7.28.4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/thomasnordquist/MQTT-Explorer/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-22 17:12:46 +01:00
dependabot[bot] 229414de28 chore(deps): bump form-data from 4.0.0 to 4.0.5 (#955)
Bumps [form-data](https://github.com/form-data/form-data) from 4.0.0 to
4.0.5.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/form-data/form-data/releases">form-data's
releases</a>.</em></p>
<blockquote>
<h2>v4.0.4</h2>
<h2><a
href="https://github.com/form-data/form-data/compare/v4.0.3...v4.0.4">v4.0.4</a>
- 2025-07-16</h2>
<h3>Commits</h3>
<ul>
<li>[meta] add <code>auto-changelog</code> <a
href="https://github.com/form-data/form-data/commit/811f68282fab0315209d0e2d1c44b6c32ea0d479"><code>811f682</code></a></li>
<li>[Tests] handle predict-v8-randomness failures in node &lt; 17 and
node &gt; 23 <a
href="https://github.com/form-data/form-data/commit/1d11a76434d101f22fdb26b8aef8615f28b98402"><code>1d11a76</code></a></li>
<li>[Fix] Switch to using <code>crypto</code> random for boundary values
<a
href="https://github.com/form-data/form-data/commit/3d1723080e6577a66f17f163ecd345a21d8d0fd0"><code>3d17230</code></a></li>
<li>[Tests] fix linting errors <a
href="https://github.com/form-data/form-data/commit/5e340800b5f8914213e4e0378c084aae71cfd73a"><code>5e34080</code></a></li>
<li>[meta] actually ensure the readme backup isn’t published <a
href="https://github.com/form-data/form-data/commit/316c82ba93fd4985af757b771b9a1f26d3b709ef"><code>316c82b</code></a></li>
<li>[Dev Deps] update <code>@ljharb/eslint-config</code> <a
href="https://github.com/form-data/form-data/commit/58c25d76406a5b0dfdf54045cf252563f2bbda8d"><code>58c25d7</code></a></li>
<li>[meta] fix readme capitalization <a
href="https://github.com/form-data/form-data/commit/2300ca19595b0ee96431e868fe2a40db79e41c61"><code>2300ca1</code></a></li>
</ul>
<h2>v4.0.3</h2>
<h2><a
href="https://github.com/form-data/form-data/compare/v4.0.2...v4.0.3">v4.0.3</a>
- 2025-06-05</h2>
<h3>Fixed</h3>
<ul>
<li>[Fix] <code>append</code>: avoid a crash on nullish values <a
href="https://redirect.github.com/form-data/form-data/issues/577"><code>[#577](https://github.com/form-data/form-data/issues/577)</code></a></li>
</ul>
<h3>Commits</h3>
<ul>
<li>[eslint] use a shared config <a
href="https://github.com/form-data/form-data/commit/426ba9ac440f95d1998dac9a5cd8d738043b048f"><code>426ba9a</code></a></li>
<li>[eslint] fix some spacing issues <a
href="https://github.com/form-data/form-data/commit/20941917f0e9487e68c564ebc3157e23609e2939"><code>2094191</code></a></li>
<li>[Refactor] use <code>hasown</code> <a
href="https://github.com/form-data/form-data/commit/81ab41b46fdf34f5d89d7ff30b513b0925febfaa"><code>81ab41b</code></a></li>
<li>[Fix] validate boundary type in <code>setBoundary()</code> method <a
href="https://github.com/form-data/form-data/commit/8d8e4693093519f7f18e3c597d1e8df8c493de9e"><code>8d8e469</code></a></li>
<li>[Tests] add tests to check the behavior of <code>getBoundary</code>
with non-strings <a
href="https://github.com/form-data/form-data/commit/837b8a1f7562bfb8bda74f3fc538adb7a5858995"><code>837b8a1</code></a></li>
<li>[Dev Deps] remove unused deps <a
href="https://github.com/form-data/form-data/commit/870e4e665935e701bf983a051244ab928e62d58e"><code>870e4e6</code></a></li>
<li>[meta] remove local commit hooks <a
href="https://github.com/form-data/form-data/commit/e6e83ccb545a5619ed6cd04f31d5c2f655eb633e"><code>e6e83cc</code></a></li>
<li>[Dev Deps] update <code>eslint</code> <a
href="https://github.com/form-data/form-data/commit/4066fd6f65992b62fa324a6474a9292a4f88c916"><code>4066fd6</code></a></li>
<li>[meta] fix scripts to use prepublishOnly <a
href="https://github.com/form-data/form-data/commit/c4bbb13c0ef669916657bc129341301b1d331d75"><code>c4bbb13</code></a></li>
</ul>
<h2>v4.0.2</h2>
<h2><a
href="https://github.com/form-data/form-data/compare/v4.0.1...v4.0.2">v4.0.2</a>
- 2025-02-14</h2>
<h3>Merged</h3>
<ul>
<li>[Fix] set <code>Symbol.toStringTag</code> when available <a
href="https://redirect.github.com/form-data/form-data/pull/573"><code>[#573](https://github.com/form-data/form-data/issues/573)</code></a></li>
<li>[Fix] set <code>Symbol.toStringTag</code> when available <a
href="https://redirect.github.com/form-data/form-data/pull/573"><code>[#573](https://github.com/form-data/form-data/issues/573)</code></a></li>
<li>fix (npmignore): ignore temporary build files <a
href="https://redirect.github.com/form-data/form-data/pull/532"><code>[#532](https://github.com/form-data/form-data/issues/532)</code></a></li>
<li>fix (npmignore): ignore temporary build files <a
href="https://redirect.github.com/form-data/form-data/pull/532"><code>[#532](https://github.com/form-data/form-data/issues/532)</code></a></li>
</ul>
<h3>Fixed</h3>
<ul>
<li>[Fix] set <code>Symbol.toStringTag</code> when available (<a
href="https://redirect.github.com/form-data/form-data/issues/573">#573</a>)
<a
href="https://redirect.github.com/form-data/form-data/issues/396"><code>[#396](https://github.com/form-data/form-data/issues/396)</code></a></li>
<li>[Fix] set <code>Symbol.toStringTag</code> when available (<a
href="https://redirect.github.com/form-data/form-data/issues/573">#573</a>)
<a
href="https://redirect.github.com/form-data/form-data/issues/396"><code>[#396](https://github.com/form-data/form-data/issues/396)</code></a></li>
<li>[Fix] set <code>Symbol.toStringTag</code> when available <a
href="https://redirect.github.com/form-data/form-data/issues/396"><code>[#396](https://github.com/form-data/form-data/issues/396)</code></a></li>
</ul>
<h3>Commits</h3>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/form-data/form-data/blob/master/CHANGELOG.md">form-data's
changelog</a>.</em></p>
<blockquote>
<h2><a
href="https://github.com/form-data/form-data/compare/v4.0.4...v4.0.5">v4.0.5</a>
- 2025-11-17</h2>
<h3>Commits</h3>
<ul>
<li>[Tests] Switch to newer v8 prediction library; enable node 24
testing <a
href="https://github.com/form-data/form-data/commit/16e00765342106876f98a1c9703314006c9e937a"><code>16e0076</code></a></li>
<li>[Dev Deps] update <code>@ljharb/eslint-config</code>,
<code>eslint</code> <a
href="https://github.com/form-data/form-data/commit/5822467f0ec21f6ad613c1c90856375e498793c7"><code>5822467</code></a></li>
<li>[Fix] set Symbol.toStringTag in the proper place <a
href="https://github.com/form-data/form-data/commit/76d0dee43933b5e167f7f09e5d9cbbd1cf911aa7"><code>76d0dee</code></a></li>
</ul>
<h2><a
href="https://github.com/form-data/form-data/compare/v4.0.3...v4.0.4">v4.0.4</a>
- 2025-07-16</h2>
<h3>Commits</h3>
<ul>
<li>[meta] add <code>auto-changelog</code> <a
href="https://github.com/form-data/form-data/commit/811f68282fab0315209d0e2d1c44b6c32ea0d479"><code>811f682</code></a></li>
<li>[Tests] handle predict-v8-randomness failures in node &lt; 17 and
node &gt; 23 <a
href="https://github.com/form-data/form-data/commit/1d11a76434d101f22fdb26b8aef8615f28b98402"><code>1d11a76</code></a></li>
<li>[Fix] Switch to using <code>crypto</code> random for boundary values
<a
href="https://github.com/form-data/form-data/commit/3d1723080e6577a66f17f163ecd345a21d8d0fd0"><code>3d17230</code></a></li>
<li>[Tests] fix linting errors <a
href="https://github.com/form-data/form-data/commit/5e340800b5f8914213e4e0378c084aae71cfd73a"><code>5e34080</code></a></li>
<li>[meta] actually ensure the readme backup isn’t published <a
href="https://github.com/form-data/form-data/commit/316c82ba93fd4985af757b771b9a1f26d3b709ef"><code>316c82b</code></a></li>
<li>[Dev Deps] update <code>@ljharb/eslint-config</code> <a
href="https://github.com/form-data/form-data/commit/58c25d76406a5b0dfdf54045cf252563f2bbda8d"><code>58c25d7</code></a></li>
<li>[meta] fix readme capitalization <a
href="https://github.com/form-data/form-data/commit/2300ca19595b0ee96431e868fe2a40db79e41c61"><code>2300ca1</code></a></li>
</ul>
<h2><a
href="https://github.com/form-data/form-data/compare/v4.0.2...v4.0.3">v4.0.3</a>
- 2025-06-05</h2>
<h3>Fixed</h3>
<ul>
<li>[Fix] <code>append</code>: avoid a crash on nullish values <a
href="https://redirect.github.com/form-data/form-data/issues/577"><code>[#577](https://github.com/form-data/form-data/issues/577)</code></a></li>
</ul>
<h3>Commits</h3>
<ul>
<li>[eslint] use a shared config <a
href="https://github.com/form-data/form-data/commit/426ba9ac440f95d1998dac9a5cd8d738043b048f"><code>426ba9a</code></a></li>
<li>[eslint] fix some spacing issues <a
href="https://github.com/form-data/form-data/commit/20941917f0e9487e68c564ebc3157e23609e2939"><code>2094191</code></a></li>
<li>[Refactor] use <code>hasown</code> <a
href="https://github.com/form-data/form-data/commit/81ab41b46fdf34f5d89d7ff30b513b0925febfaa"><code>81ab41b</code></a></li>
<li>[Fix] validate boundary type in <code>setBoundary()</code> method <a
href="https://github.com/form-data/form-data/commit/8d8e4693093519f7f18e3c597d1e8df8c493de9e"><code>8d8e469</code></a></li>
<li>[Tests] add tests to check the behavior of <code>getBoundary</code>
with non-strings <a
href="https://github.com/form-data/form-data/commit/837b8a1f7562bfb8bda74f3fc538adb7a5858995"><code>837b8a1</code></a></li>
<li>[Dev Deps] remove unused deps <a
href="https://github.com/form-data/form-data/commit/870e4e665935e701bf983a051244ab928e62d58e"><code>870e4e6</code></a></li>
<li>[meta] remove local commit hooks <a
href="https://github.com/form-data/form-data/commit/e6e83ccb545a5619ed6cd04f31d5c2f655eb633e"><code>e6e83cc</code></a></li>
<li>[Dev Deps] update <code>eslint</code> <a
href="https://github.com/form-data/form-data/commit/4066fd6f65992b62fa324a6474a9292a4f88c916"><code>4066fd6</code></a></li>
<li>[meta] fix scripts to use prepublishOnly <a
href="https://github.com/form-data/form-data/commit/c4bbb13c0ef669916657bc129341301b1d331d75"><code>c4bbb13</code></a></li>
</ul>
<h2><a
href="https://github.com/form-data/form-data/compare/v4.0.1...v4.0.2">v4.0.2</a>
- 2025-02-14</h2>
<h3>Merged</h3>
<ul>
<li>[Fix] set <code>Symbol.toStringTag</code> when available <a
href="https://redirect.github.com/form-data/form-data/pull/573"><code>[#573](https://github.com/form-data/form-data/issues/573)</code></a></li>
<li>[Fix] set <code>Symbol.toStringTag</code> when available <a
href="https://redirect.github.com/form-data/form-data/pull/573"><code>[#573](https://github.com/form-data/form-data/issues/573)</code></a></li>
<li>fix (npmignore): ignore temporary build files <a
href="https://redirect.github.com/form-data/form-data/pull/532"><code>[#532](https://github.com/form-data/form-data/issues/532)</code></a></li>
<li>fix (npmignore): ignore temporary build files <a
href="https://redirect.github.com/form-data/form-data/pull/532"><code>[#532](https://github.com/form-data/form-data/issues/532)</code></a></li>
</ul>
<h3>Fixed</h3>
<ul>
<li>[Fix] set <code>Symbol.toStringTag</code> when available (<a
href="https://redirect.github.com/form-data/form-data/issues/573">#573</a>)
<a
href="https://redirect.github.com/form-data/form-data/issues/396"><code>[#396](https://github.com/form-data/form-data/issues/396)</code></a></li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/form-data/form-data/commit/68ff7dda8834d6de095a7008cef0e03bc252ca98"><code>68ff7dd</code></a>
v4.0.5</li>
<li><a
href="https://github.com/form-data/form-data/commit/5822467f0ec21f6ad613c1c90856375e498793c7"><code>5822467</code></a>
[Dev Deps] update <code>@ljharb/eslint-config</code>,
<code>eslint</code></li>
<li><a
href="https://github.com/form-data/form-data/commit/76d0dee43933b5e167f7f09e5d9cbbd1cf911aa7"><code>76d0dee</code></a>
[Fix] set Symbol.toStringTag in the proper place</li>
<li><a
href="https://github.com/form-data/form-data/commit/16e00765342106876f98a1c9703314006c9e937a"><code>16e0076</code></a>
[Tests] Switch to newer v8 prediction library; enable node 24
testing</li>
<li><a
href="https://github.com/form-data/form-data/commit/41996f5ac73a867046d48512cab62e64fc846dad"><code>41996f5</code></a>
v4.0.4</li>
<li><a
href="https://github.com/form-data/form-data/commit/316c82ba93fd4985af757b771b9a1f26d3b709ef"><code>316c82b</code></a>
[meta] actually ensure the readme backup isn’t published</li>
<li><a
href="https://github.com/form-data/form-data/commit/2300ca19595b0ee96431e868fe2a40db79e41c61"><code>2300ca1</code></a>
[meta] fix readme capitalization</li>
<li><a
href="https://github.com/form-data/form-data/commit/811f68282fab0315209d0e2d1c44b6c32ea0d479"><code>811f682</code></a>
[meta] add <code>auto-changelog</code></li>
<li><a
href="https://github.com/form-data/form-data/commit/5e340800b5f8914213e4e0378c084aae71cfd73a"><code>5e34080</code></a>
[Tests] fix linting errors</li>
<li><a
href="https://github.com/form-data/form-data/commit/1d11a76434d101f22fdb26b8aef8615f28b98402"><code>1d11a76</code></a>
[Tests] handle predict-v8-randomness failures in node &lt; 17 and node
&gt; 23</li>
<li>Additional commits viewable in <a
href="https://github.com/form-data/form-data/compare/v4.0.0...v4.0.5">compare
view</a></li>
</ul>
</details>
<details>
<summary>Maintainer changes</summary>
<p>This version was pushed to npm by <a
href="https://www.npmjs.com/~ljharb">ljharb</a>, a new releaser for
form-data since your current version.</p>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=form-data&package-manager=npm_and_yarn&previous-version=4.0.0&new-version=4.0.5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the
[Security Alerts
page](https://github.com/thomasnordquist/MQTT-Explorer/network/alerts).

</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-12-22 17:02:27 +01:00
Copilot 6c041cba02 Security hardening: authentication, input validation, OWASP compliance, architecture improvements, and CSP fixes for browser mode (#942) 2025-12-22 16:52:42 +01:00
Thomas Nordquist a7136bd572 chore: update LICENSE.md to clarify distribution terms 2025-12-22 16:43:44 +01:00
Copilot a5629b8c77 chore: add macOS notarization support for DMG builds (#944) 2025-12-21 17:36:01 +01:00
Thomas Nordquist da122e06f1 Update AI contribution guidelines for PRs
Added requirements for PR screenshots and build error resolution.
2025-12-21 17:21:53 +01:00
Copilot e0a79f61af docs: add semantic commit requirements and quality standards to agent instructions (#946) 2025-12-21 16:53:03 +01:00
26ed0aadd2 Upgrade to Node.js 24, update dependencies, migrate configs to ES modules, replace ts-node with tsx, upgrade React/Material-UI, and update GitHub Actions (#940)
## Update Dependencies, Node.js 24, and Migrate to ES Modules

This PR updates the project dependencies, Node.js version, workflows,
and migrates from CommonJS to ES modules.

### Checklist

#### Phase 1: Assessment and Configuration
- [x] Assess current project structure and dependencies
- [x] Update Node.js version requirements (>=24 for builds, >=20 for
runtime)
- [x] Update GitHub workflow files to use Node 24
- [x] Update Dockerfile to use Node 24

#### Phase 2: TypeScript Configuration for ES Modules
- [x] Update root tsconfig.json to use ES2020 target with CommonJS
modules
- [x] Update backend/tsconfig.json to use ES2020 target with CommonJS
modules
- [x] Update app/tsconfig.json to use ES2020 target with ESNext modules

#### Phase 3: Update Dependencies
- [x] Update root package.json dependencies to latest compatible
versions
- [x] Update app/package.json dependencies to latest compatible versions
- [x] Update backend/package.json dependencies to latest compatible
versions
- [x] Run yarn install to update lockfile

#### Phase 4: Convert CommonJS to ES Modules
- [x] Convert webpack config files to ES modules (.js → .mjs)
- [x] Convert prettier.config.js to ES modules
- [x] Update TypeScript files with CommonJS require() to use ES imports
- [x] Fix breaking changes from dependency API updates

#### Phase 5: Replace ts-node with tsx
- [x] Replace ts-node with tsx in all package.json scripts
- [x] Update root package.json to use tsx for prepare-release and
package scripts
- [x] Update backend package.json to use tsx with mocha
- [x] Update app package.json to use tsx with mocha
- [x] Update script shebangs to use tsx
- [x] Add tsx to devDependencies, remove ts-node

#### Phase 6: Upgrade React and Material-UI
- [x] Upgrade React from 16.14.0 to 18.3.1
- [x] Upgrade React-DOM from 16.14.0 to 18.3.1
- [x] Migrate from @material-ui (v4) to @mui/material (v5)
- [x] Add @emotion/react and @emotion/styled (required for MUI v5)
- [x] Update all import paths from @material-ui/* to @mui/*
- [x] Update theme creation from createMuiTheme to createTheme
- [x] Update palette.type to palette.mode
- [x] Update ReactDOM.render to ReactDOM.createRoot (React 18)
- [x] Update ThemeProvider import to use @mui/material/styles
- [x] Add @mui/styles for withStyles compatibility
- [x] Separate Theme and withStyles imports correctly
- [x] Replace fade with alpha in theme styles
- [x] Replace ExpansionPanel with Accordion
- [x] Fix all component imports from wrong modules
- [x] Replace withTheme HOC with useTheme hook
- [x] Replace theme.palette.text.hint with theme.palette.text.secondary
- [x] Update all Redux reducers for Redux v5 compatibility

#### Phase 7: Fix All TypeScript Errors
- [x] Fix Dialog disableBackdropClick removal (use onClose handler)
- [x] Fix Button classes.label removal (use sx prop)
- [x] Fix Select onChange signature (MUI v5 API change)
- [x] Fix Snackbar onClose signature (MUI v5 API change)
- [x] Fix ClickAwayListener onClickAway signature (MUI v5 API change)
- [x] Fix ReactResizeDetector (migrate to useResizeDetector hook)
- [x] Fix Redux connect + withStyles type compatibility (use type
assertions)
- [x] Fix all connected component prop type errors
- [x] Add children prop to ErrorBoundary
- [x] Add parameter types to callbacks

#### Phase 8: Build and Test
- [x] Run yarn build -  **SUCCESSFUL with 0 errors, 1 minor warning**
- [x] Run yarn test -  **All 27 tests passing (5 app + 22 backend)**

#### Phase 9: Update All GitHub Actions
- [x] Update Node.js to 24 in copilot-setup.yml workflow
- [x] Update Node.js to 24 in update-website.yml workflow
- [x] Update docker/build-push-action from v5 to v6
- [x] Replace deprecated tibdex/github-app-token@v2 with
actions/create-github-app-token@v1
- [x] All other actions already at latest versions (v4 for GitHub
actions, v3 for Docker actions)

#### Phase 10: Final Validation
- [x] All TypeScript compilation errors fixed
- [x] All tests passing
- [x] Build completes successfully
- [x] Clarified Node.js engine requirements per use case
- [x] All GitHub Actions updated to latest versions

### Node.js Version Requirements

This project has different Node.js requirements depending on the use
case:

#### Building the Electron App (Root package.json)
- **Required:** Node.js >= 24
- **Why:** Build tools like @electron/notarize and semantic-release
require Node.js 24+
- **Affected files:** `/package.json`

#### Running the Backend/Server (Backend package.json)
- **Required:** Node.js >= 20
- **Why:** The MQTT server runtime is compatible with Node.js 20+
- **Affected files:** `/backend/package.json`

#### Frontend App (App package.json)  
- **Required:** Node.js >= 20
- **Why:** React and webpack tools are compatible with Node.js 20+
- **Affected files:** `/app/package.json`

### Summary of All Changes

**Major Dependency Updates:**
- TypeScript: 4.5.5 → 5.9.3
- Node.js: >=24 for builds, >=20 for runtime
- React: 16.14.0 → 18.3.1
- React-DOM: 16.14.0 → 18.3.1
- Redux: 4.2.1 → 5.0.1
- @material-ui/core → @mui/material 5.18.0
- @material-ui/icons → @mui/icons-material 5.18.0
- mqtt: 4.3.6 → 5.14.1
- axios: 0.28.0 → 1.13.2
- redux-thunk: 2.3.0 → 3.1.0
- electron-builder: 24.13.3 → 26.0.12
- @electron/notarize: 3.1.1 (latest)
- semantic-release: 25.0.2 (latest)
- react-resize-detector: migrated to useResizeDetector hook
- 50+ other dependencies

**GitHub Actions Updated:**
-  actions/checkout@v4 (latest)
-  actions/setup-node@v4 (latest) - Now uses Node 24 in all workflows
-  actions/cache@v4 (latest)
-  actions/upload-artifact@v4 (latest)
-  docker/build-push-action: v5 → **v6** (latest)
-  docker/login-action@v3 (latest)
-  docker/setup-buildx-action@v3 (latest)
-  cycjimmy/semantic-release-action@v4 (latest)
-  tibdex/github-app-token@v2 → **actions/create-github-app-token@v1**
(official replacement for deprecated action)
-  stefanzweifel/git-auto-commit-action@v5 (latest)
-  hkusu/s3-upload-action@v2 (latest)

**All TypeScript Errors Fixed:**
- Removed disableBackdropClick from Dialog (deprecated in MUI v5)
- Removed Button classes.label (deprecated in MUI v5)
- Updated all event handler signatures to match MUI v5 APIs
- Migrated ReactResizeDetector component to useResizeDetector hook
- Added type assertions for Redux connect + withStyles compatibility
- Fixed all connected component prop passing
- Added missing children props to components

**Final Result:**
 **0 TypeScript errors**
 **27/27 tests passing**
 **Build successful**
 **Node 24 for builds, Node 20+ for runtime**
 **All GitHub Actions updated to latest versions**
⚠️ 1 minor warning (source map parsing in ace-builds dependency)

<!-- START COPILOT ORIGINAL PROMPT -->



<details>

<summary>Original prompt</summary>

> 
> ----
> 
> *This section details on the original issue you should resolve*
> 
> <issue_title>update dependencies</issue_title>
> <issue_description>- update to nodejs 24
> - update npm dependencies
> - update workflows
> - change from commons to esmodules</issue_description>
> 
> ## Comments on the Issue (you are @copilot in this section)
> 
> <comments>
> </comments>
> 


</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes thomasnordquist/MQTT-Explorer#939

<!-- START COPILOT CODING AGENT TIPS -->
---

💬 We'd love your input! Share your thoughts on Copilot coding agent in
our [2 minute survey](https://gh.io/copilot-coding-agent-survey).

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com>
Co-authored-by: Thomas Nordquist <thomasnordquist@users.noreply.github.com>
2025-12-21 10:20:08 +01:00
Thomas Nordquist 578bb510f9 Upgrade Node.js version from 20 to 24 2025-12-21 09:59:50 +01:00
Copilot e725b1d012 Fix expandTopic selector, restore and streamline comprehensive UI tests (#938) 2025-12-20 23:26:15 +01:00
Copilotandthomasnordquist c55c3a8245 Fix UI tests: correct expandTopic parameter order and CI workflow (#936)
Fixes TypeScript compilation errors in UI tests and resolves CI workflow
configuration issue.

## Changes Made

### 1. Fixed expandTopic parameter order in ui-tests.spec.ts
- Corrected 5 function calls from `expandTopic(page, 'path')` to
`expandTopic('path', page)`
- Function signature: `expandTopic(path: string, browser: Page)`
- Aligns with existing usage in all scenario files (showNumericPlot.ts,
publishTopic.ts, etc.)

### 2. Fixed CI workflow configuration
- Updated `.github/workflows/tests.yml` to checkout PR code instead of
base branch
- Added `ref: ${{ github.event.pull_request.head.sha }}` to all 4
checkout actions
- The `pull_request_target` event defaults to checking out the base
branch; this fix ensures CI tests the PR's code

## Root Cause

The CI workflow was testing the base branch (master) which still had the
wrong parameter order, while the PR had the correct fix. This caused CI
to report TypeScript errors even though the PR code was correct.

## Testing

-  TypeScript compilation passes locally (`tsc` and `yarn build`)
-  Parameter order matches function signature and codebase conventions
-  CI workflow now correctly tests PR code
-  All 4 CI jobs (test, ui-tests, demo-video, test-browser) will use
corrected code

<!-- START COPILOT ORIGINAL PROMPT -->



<details>

<summary>Original prompt</summary>

> 
> ----
> 
> *This section details on the original issue you should resolve*
> 
> <issue_title>Fix tests</issue_title>
> <issue_description>- fix backend tests
> - fix UI tests</issue_description>
> 
> ## Comments on the Issue (you are @copilot in this section)
> 
> <comments>
> </comments>
> 


</details>



<!-- START COPILOT CODING AGENT SUFFIX -->

- Fixes thomasnordquist/MQTT-Explorer#935

<!-- START COPILOT CODING AGENT TIPS -->
---

 Let Copilot coding agent [set things up for
you](https://github.com/thomasnordquist/MQTT-Explorer/issues/new?title=+Set+up+Copilot+instructions&body=Configure%20instructions%20for%20this%20repository%20as%20documented%20in%20%5BBest%20practices%20for%20Copilot%20coding%20agent%20in%20your%20repository%5D%28https://gh.io/copilot-coding-agent-tips%29%2E%0A%0A%3COnboard%20this%20repo%3E&assignees=copilot)
— coding agent works faster and does higher quality work when set up for
your repo.

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com>
2025-12-20 19:34:34 +01:00
160 changed files with 8470 additions and 5053 deletions
+21 -3
View File
@@ -2,7 +2,7 @@
"name": "MQTT Explorer Development",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspace",
"workspaceFolder": "/workspace/MQTT-Explorer",
"customizations": {
"vscode": {
@@ -15,7 +15,6 @@
],
"settings": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"typescript.tsdk": "node_modules/typescript/lib",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
@@ -24,7 +23,7 @@
}
},
"forwardPorts": [3000, 8080, 1883],
"forwardPorts": [3000, 8080, 1883, 5900, 6080],
"portsAttributes": {
"3000": {
"label": "MQTT Explorer Server",
@@ -37,10 +36,29 @@
"1883": {
"label": "MQTT Broker",
"onAutoForward": "ignore"
},
"5900": {
"label": "VNC Server",
"onAutoForward": "ignore"
},
"6080": {
"label": "noVNC Web Client",
"onAutoForward": "notify"
}
},
"postCreateCommand": "yarn install",
"postStartCommand": "sudo apt-get update && sudo apt-get install -y mosquitto xvfb x11vnc ffmpeg tmux python3 python3-pip && sudo pip3 install --break-system-packages websockify",
"features": {
"ghcr.io/devcontainers/features/common-utils:2": {
"installZsh": true,
"installOhMyZsh": true,
"upgradePackages": true
}
},
"remoteUser": "node"
}
+7 -5
View File
@@ -2,9 +2,9 @@ version: '3.8'
services:
app:
image: mcr.microsoft.com/devcontainers/javascript-node:20
image: mcr.microsoft.com/devcontainers/javascript-node:24
volumes:
- ../..:/workspace:cached
- ..:/workspaces/MQTT-Explorer:cached
command: sleep infinity
network_mode: service:mosquitto
environment:
@@ -14,8 +14,10 @@ services:
mosquitto:
image: eclipse-mosquitto:2
ports:
- "1883:1883"
- "3000:3000"
- "8080:8080"
- '1883:1883'
- '3000:3000'
- '8080:8080'
- '5900:5900'
- '6080:6080'
volumes:
- ./mosquitto.conf:/mosquitto/config/mosquitto.conf:ro
+177 -353
View File
@@ -1,383 +1,207 @@
# GitHub Copilot Agent Instructions for MQTT Explorer
## Overview
## Debugging Browser Mode
MQTT Explorer is an Electron-based desktop application for exploring MQTT brokers. It provides a comprehensive UI for connecting to MQTT brokers, browsing topics, and analyzing message flows.
### Prerequisites
- Node.js 24 or higher
- Yarn package manager
- Running Mosquitto MQTT broker (for testing)
## Technology Stack
### Development Mode (with Hot Reload)
- **Frontend**: React 16.x with Material-UI
- **Backend**: Node.js with TypeScript
- **Desktop Framework**: Electron 29.x
- **MQTT Client**: [mqttjs](https://github.com/mqttjs/MQTT.js) v4.x
- **State Management**: Redux with redux-thunk
- **Build Tools**: webpack, TypeScript compiler
- **Testing**: Mocha + Chai for unit tests, Playwright for MCP introspection tests
1. **Set credentials (required):**
```bash
export MQTT_EXPLORER_USERNAME=admin
export MQTT_EXPLORER_PASSWORD=your_password
```
## Project Setup
2. **Start development servers:**
```bash
yarn dev:server
```
This runs two servers in parallel:
- Backend server on http://localhost:3000 (serves API, WebSocket, authentication)
- Webpack dev server on http://localhost:8080 (serves frontend with hot reload)
### Building and Running
3. **Access the application:**
- Navigate to http://localhost:8080 (NOT :3000)
- Webpack dev server proxies API/WebSocket requests to backend on port 3000
- Hot reload enabled - changes to React components update automatically
### Production Mode (Production Build)
1. **Build the browser version:**
```bash
yarn build:server
```
This compiles TypeScript and builds the optimized webpack bundle
2. **Start the server:**
```bash
# Set credentials (required) - these are for the browser login page
export MQTT_EXPLORER_USERNAME=admin
export MQTT_EXPLORER_PASSWORD=your_password
# Start server
yarn start:server
# OR: node dist/src/server.js
```
Server will run on http://localhost:3000 (serves both frontend and backend)
3. **Login to the application:**
- Navigate to http://localhost:3000
- Enter the username and password you set in the environment variables
- Click "LOGIN" button
- After successful login, the main application will load
- The MQTT Connection modal will appear where you can configure broker connections
### Debugging with Browser DevTools
1. **Open browser DevTools:**
- Navigate to http://localhost:3000
- Press F12 or right-click → Inspect
2. **Check Console tab:**
- Look for JavaScript errors
- CSP (Content Security Policy) errors indicate security header issues
- Network errors indicate API/WebSocket connection issues
3. **Check Network tab:**
- Verify static assets load correctly (JS bundles, CSS)
- Check WebSocket connection status
- Monitor API calls for authentication issues
4. **Common Issues:**
**Blank page / CSP errors:**
- Symptom: Console shows `EvalError: ... violates Content Security Policy`
- Cause: webpack runtime requires `unsafe-eval` for code splitting
- Fix: Add `'unsafe-eval'` to `scriptSrc` in `src/server.ts` helmet config
**Authentication loop:**
- Symptom: Login dialog keeps reappearing
- Cause: WebSocket authentication failing
- Debug: Check browser Network tab → WS → Messages
- Check: Server logs for authentication errors
**Theme errors:**
- Symptom: App loads but styling is broken
- Cause: Material-UI theme not loading correctly
- Check: Console for theme-related errors
- Verify: Both ThemeProvider and LegacyThemeProvider in `app/src/index.tsx`
**Expected console warnings (non-fatal):**
- React 18 type warnings with Material-UI v5 components (dozens of "Failed prop type" warnings)
- `TypeError: Cannot read properties of undefined (reading 'on')` from IpcRendererEventBus - this is expected in browser mode as there's no Electron IPC
- MUI locale warnings for `en-US` - expected, app uses available locales
- `componentWillReceiveProps` deprecation warnings - from legacy TreeComponent
- ACE editor autocomplete warnings - expected, features not imported
- CSP worker violation for ACE editor - known issue, editor still functions
These warnings don't prevent the application from functioning correctly.
### Using Playwright for Automated Testing
```bash
# Install dependencies
yarn install
# Build the project
yarn build
# Set password for browser testing
# Start server in background
export MQTT_EXPLORER_USERNAME=admin
export MQTT_EXPLORER_PASSWORD=secretpassword
export MQTT_EXPLORER_PASSWORD=test123
node dist/src/server.js &
# Start the application
yarn start
# Start in development mode
yarn dev
# Use Playwright browser tool (in Copilot agent context)
playwright-browser_navigate http://localhost:3000
playwright-browser_take_screenshot --filename debug.png
playwright-browser_console_messages # Check for errors
```
### Running with MCP Introspection (for testing)
### Expected UI Flow
```bash
# Build first
yarn build
# Start with MCP introspection enabled
electron . --enable-mcp-introspection
# Or with custom port
electron . --enable-mcp-introspection --remote-debugging-port=9223
```
## Writing Tests
### Requirements for All Tests
1. **Tests MUST be deterministic** - They should produce the same results every time they run
2. **Tests MUST be independent** - Each test should be able to run in isolation without depending on other tests
3. **Include screenshots** - Visual verification is required for UI changes
4. **Handle asynchronous operations properly** - This is an MQTT message queue tool
### Best Practices for UI Tests
#### 1. Use Given-When-Then Pattern
Structure tests with clear Given-When-Then comments to make them readable:
```typescript
it('Given a JSON message sent to topic foo/bar/baz, the tree should display nested topics', async function () {
// Given: Mock MQTT publishes JSON to foo/bar/baz
// When: We wait for the topic to appear in the tree
// Then: Topic hierarchy should be visible (foo -> bar -> baz)
})
```
#### 2. Wait for Elements, Don't Use Fixed Delays
Prefer `waitFor` over `sleep` whenever possible:
```typescript
// ✓ Good: Wait for specific element
const topic = await page.locator('span[data-test-topic="kitchen"]')
await topic.waitFor({ state: 'visible', timeout: 5000 })
// ✗ Bad: Fixed delay without verification
await sleep(5000)
```
#### 3. Use Meaningful Assertions
Every test should have explicit assertions that verify the expected state:
```typescript
// ✓ Good: Explicit assertion with meaningful message
const treeNodes = await page.locator('[class*="TreeNode"]')
const count = await treeNodes.count()
expect(count).to.be.greaterThan(0, 'Topic tree should contain nodes')
// ✗ Bad: No assertion, only screenshot
await page.screenshot({ path: 'test.png' })
```
#### 4. Test Data-Driven Scenarios
Write tests that describe the data flow:
```typescript
it('Given messages sent to livingroom/lamp/state and livingroom/lamp/brightness, both should appear under livingroom/lamp', async function () {
// Test implementation verifies the specific data flow
})
```
#### 5. Use Data Test Attributes
Leverage `data-test-*` attributes for reliable selectors:
```typescript
// ✓ Good: Use data-test attributes
const topic = await page.locator('span[data-test-topic="kitchen"]')
// ⚠ Acceptable: Use role/text when data attributes aren't available
const button = await page.locator('//button/span[contains(text(),"Connect")]')
// ✗ Bad: Rely on CSS classes that may change
const topic = await page.locator('.MuiTreeItem-label')
```
#### 6. Verify Multiple Aspects
Test should verify both state and UI:
```typescript
// Verify the action completed
const isVisible = await disconnectButton.isVisible()
expect(isVisible).to.be.true
// Capture screenshot for visual verification
await page.screenshot({ path: 'test-screenshot-connection.png' })
```
#### 7. Handle MQTT Asynchronous Nature
Account for message propagation time:
```typescript
// Publish message
await mockClient.publish('topic/name', 'value')
// Wait for UI to update
await page.locator(`text="value"`).waitFor({ timeout: 5000 })
// Verify state
const value = await page.textContent('.message-value')
expect(value).toBe('value')
```
### Handling MQTT Asynchronous Operations
MQTT is inherently asynchronous. When writing tests:
- **Wait for message propagation**: Use proper wait strategies (e.g., `await page.waitForSelector()`, `await sleep()`)
- **Don't assume immediate updates**: Messages take time to send, receive, and update the UI
- **Use event-based waiting**: Wait for specific UI elements or state changes rather than fixed timeouts when possible
- **Account for network latency**: MQTT broker communication involves network round trips
### Example Test Pattern
```typescript
// 1. Perform action (e.g., publish message)
await publishMessage(topic, payload)
// 2. Wait for UI to update (not just arbitrary sleep)
await page.waitForSelector(`text="${expectedValue}"`, { timeout: 5000 })
// 3. Verify state
const value = await page.textContent('.message-value')
expect(value).toBe(expectedValue)
// 4. Take screenshot for verification
await page.screenshot({ path: 'test-result.png' })
```
### Running Tests
```bash
# Run all tests
yarn test
# Run specific test suites
yarn test:app
yarn test:backend
yarn test:mcp
# Run linters
yarn lint
yarn lint:fix
```
### Running UI Tests (yarn test:ui)
The UI tests require specific setup in the test environment:
**Prerequisites:**
1. **Xvfb (X Virtual Framebuffer)** - Required for headless Electron testing
```bash
# Start Xvfb on display :99
Xvfb :99 -screen 0 1024x720x24 -ac &
export DISPLAY=:99
```
2. **Mosquitto MQTT Broker** - Required for MQTT message testing
```bash
# Install mosquitto
sudo apt-get install -y mosquitto mosquitto-clients
1. **Login Page** (https://github.com/user-attachments/assets/383305e1-2169-433c-a668-5a05da0c343a)
- Enter username and password from environment variables
- Click "LOGIN" button
# Start mosquitto service
sudo systemctl start mosquitto
2. **Main Application After Login** (https://github.com/user-attachments/assets/cc4d665f-2665-4289-b2fc-dc4986f9ab5b)
- Application loads with sidebar, topic tree, value panel, and publish panel
- MQTT Connection modal appears automatically for first-time setup
- Configure broker connection (host, port, credentials, etc.)
- Click "CONNECT" to establish MQTT connection
# Verify it's running on port 1883
sudo systemctl status mosquitto
```
3. **Application Features:**
- Topic tree on the left shows MQTT topic hierarchy
- Value panel shows selected topic's message content
- Publish panel allows sending MQTT messages
- Charts panel for numeric value visualization
- Settings drawer for app configuration
3. **@types/node** - Required for TypeScript compilation
### Debugging WebSocket Connection
1. **Check server logs:**
```bash
yarn add -D @types/node
node dist/src/server.js 2>&1 | tee server.log
```
**Running UI Tests:**
```bash
# Build the application first
yarn build
2. **Check browser WebSocket:**
- DevTools → Network → WS tab
- Look for socket.io connection
- Check Messages tab for authentication handshake
# Run UI tests with proper display
DISPLAY=:99 yarn test:ui
3. **Common WebSocket issues:**
- CORS errors: Check `ALLOWED_ORIGINS` environment variable
- Authentication errors: Verify credentials in sessionStorage
- Connection refused: Server not running or port blocked
### Development vs Production
**Development mode:**
```bash
yarn dev:server
# Runs webpack-dev-server with hot reload
# More verbose error messages
# Source maps enabled
```
**Common Issues:**
- **"Timeout exceeded" in before hook**: Mosquitto is not running or not accessible on port 1883
- **"Cannot find type definition file for 'node'"**: Run `yarn add -D @types/node`
- **Electron fails to launch**: Xvfb is not running or DISPLAY variable not set
- **Tests hang**: Check if old Electron/mosquitto processes are still running and kill them
**Environment Cleanup:**
**Production mode:**
```bash
# Kill old Electron processes
ps aux | grep electron | grep -v grep | awk '{print $2}' | xargs kill -9 2>/dev/null
# Kill old mosquitto processes (if running custom instance)
ps aux | grep mosquitto | grep -v grep | awk '{print $2}' | xargs kill -9 2>/dev/null
NODE_ENV=production yarn build:server
NODE_ENV=production node dist/src/server.js
# Minified bundles
# Generic error messages (security)
# HSTS enabled
```
## MCP Introspection Testing
The project supports MCP (Model Context Protocol) for automated testing with Playwright:
- Use `yarn test:mcp` to run automated UI tests
- Tests launch the app with remote debugging enabled on port 9222
- Connect to `http://localhost:9222` via Chrome DevTools Protocol
## Project Structure
- `app/` - Frontend React application
- `backend/` - Backend models, tests, and connection management
- `src/` - Electron main process and bindings
- `src/spec/` - Test specifications including MCP introspection tests
## Code Style and Formatting
### Linting
The project uses TSLint with Airbnb config and Prettier for code formatting:
```bash
# Run all linters
yarn lint
# Run linters individually
yarn lint:prettier # Check Prettier formatting
yarn lint:tslint # Check TSLint rules
yarn lint:spellcheck # Check spelling in code
# Auto-fix issues
yarn lint:fix # Fix TSLint and Prettier issues
yarn lint:tslint:fix # Fix TSLint issues only
yarn lint:prettier:fix # Fix Prettier issues only
```
### Code Style Rules
- **Semicolons**: Never use semicolons (enforced by TSLint and Prettier)
- **Quotes**: Single quotes for strings
- **Indentation**: 2 spaces
- **Line length**: Maximum 120 characters (Prettier) / 200 characters (TSLint)
- **Arrow functions**: No parentheses for single parameters (`x => x + 1`)
- **Trailing commas**: Required for multiline objects and arrays (ES5 compatible)
### TypeScript Guidelines
- Enable strict null checks and no implicit any
- Use TypeScript interfaces for data structures
- Prefer `const` over `let`, avoid `var`
- Use type inference when possible, explicit types when clarity is needed
## Dependency Management
### Adding Dependencies
```bash
# Add to root project
yarn add <package-name>
# Add to app (frontend)
cd app && yarn add <package-name>
# Add to backend
cd backend && yarn add <package-name>
# Add dev dependencies
yarn add -D <package-name>
```
### Important Dependency Notes
- Main dependencies are in the root `package.json`
- Frontend React app has its own dependencies in `app/package.json`
- Backend models and logic have dependencies in `backend/package.json`
- Always use `--frozen-lockfile` in CI to ensure reproducible builds
- Run `yarn install` after pulling changes that modify `yarn.lock`
## Debugging
### Development Mode
```bash
# Start with hot reload for frontend
yarn dev
# This runs two processes in parallel:
# 1. webpack-dev-server for the React app (port varies)
# 2. Electron in development mode with the --development flag
```
### Debugging TypeScript
- Source maps are enabled in `tsconfig.json`
- Use `ts-node` for running TypeScript files directly
- Backend tests can be debugged with: `cd backend && yarn test-inspect`
### Common Issues
- **Build fails**: Clear `dist/` and `app/build/` directories, then rebuild
- **Electron won't start**: Ensure `yarn build` completed successfully
- **Tests fail**: Check if MQTT broker (mosquitto) is running for integration tests
- **UI not updating**: In dev mode, ensure webpack-dev-server is running
## Deployment and Packaging
### Creating Releases
```bash
# Prepare release (updates version, changelog)
yarn prepare-release
# Package the application for distribution
yarn package
# Package with Docker (for consistent builds)
yarn package-with-docker
```
### Release Workflow
- **Beta releases**: Create PR to `beta` branch with "feat:" or "fix:" commits
- **Production releases**: Create PR to `release` branch with "feat:" or "fix:" commits
- Semantic release automatically handles versioning and changelog
- Builds are created for Windows, macOS, and Linux
### Build Artifacts
- Output directory: `build/`
- Supported formats: DMG (macOS), EXE/NSIS (Windows), AppImage/Snap (Linux), AppX (Windows Store)
- Code signing is configured via `res/` directory certificates and provisioning profiles
After `yarn build:server`, check:
- `dist/src/server.js` - Compiled server code
- `app/build/*.js` - Webpack bundles
- `app/build/index.html` - Entry point HTML
## Important Notes
### Troubleshooting Checklist
- Always run `yarn build` before starting the application
- The app uses Electron (see `package.json` for version)
- MQTT communication is handled via [mqttjs](https://github.com/mqttjs/MQTT.js)
- All code changes should pass linting (`yarn lint`)
- Node.js version requirement: >= 20
- The project uses workspace-like structure with separate package.json files for app and backend
- [ ] Node.js version >=24
- [ ] `yarn install` completed without errors
- [ ] TypeScript compilation successful (`npx tsc`)
- [ ] Webpack build successful (check `app/build/` directory)
- [ ] Server starts without errors
- [ ] Can access http://localhost:3000
- [ ] Login dialog appears
- [ ] No CSP errors in console
- [ ] WebSocket connects successfully
- [ ] App renders after login
### Security Considerations
When debugging, be aware that:
- `unsafe-eval` in CSP is required for webpack but reduces security
- Credentials should never be hardcoded (use environment variables)
- In production, use HTTPS with a reverse proxy (nginx/Apache)
- Rate limiting is active (5 auth attempts per 15 min per IP)
- File upload size limit is 16MB
### Related Files
- `src/server.ts` - Express server with security middleware
- `app/webpack.browser.config.mjs` - Browser-specific webpack config
- `app/src/browserEventBus.ts` - Socket.io client for browser mode
- `app/src/components/BrowserAuthWrapper.tsx` - Authentication dialog
- `app/src/index.tsx` - React app entry point with theme providers
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
uses: docker/build-push-action@v6
with:
context: .
push: true
@@ -1,25 +1,38 @@
name: Copilot Agent Setup
name: 'Copilot Setup Steps'
# Automatically run the setup steps when they are changed to allow for easy validation, and
# allow manual testing through the repository's "Actions" tab
on:
workflow_call:
workflow_dispatch:
push:
paths:
- .github/workflows/copilot-setup-steps.yml
pull_request:
paths:
- .github/workflows/copilot-setup-steps.yml
jobs:
setup:
copilot-setup-steps:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y xvfb mosquitto
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '24'
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- name: Cache yarn dependencies
uses: actions/cache@v4
id: yarn-cache
@@ -31,9 +44,6 @@ jobs:
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies
run: yarn install --frozen-lockfile
- name: Build project
run: yarn build
run: yarn
+7 -4
View File
@@ -28,14 +28,14 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24
- run: npm install -g yarn
- run: yarn
- id: create_token # get ReleaseBot access token
uses: tibdex/github-app-token@v2
uses: actions/create-github-app-token@v1
with:
app_id: ${{ secrets.RELEASE_BOT_APP_ID }}
private_key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}
app-id: ${{ secrets.RELEASE_BOT_APP_ID }}
private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}
- name: Semantic Release
uses: cycjimmy/semantic-release-action@v4
id: semantic # Need an `id` for output variables
@@ -50,4 +50,7 @@ jobs:
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
+9 -1
View File
@@ -12,6 +12,8 @@ jobs:
options: --user root
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Install Packages
run: yarn install --frozen-lockfile
- name: Build
@@ -28,6 +30,8 @@ jobs:
options: --user root
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Install Packages
run: yarn install --frozen-lockfile
- name: Build
@@ -53,6 +57,8 @@ jobs:
options: --user root
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Install Packages
run: yarn install --frozen-lockfile
- name: Build
@@ -90,10 +96,12 @@ jobs:
--health-retries 5
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '24'
- name: Install Dependencies
run: yarn install --frozen-lockfile
- name: Build Browser Mode
+1 -1
View File
@@ -11,7 +11,7 @@ jobs:
ref: gh-pages
- uses: actions/setup-node@v4
with:
node-version: 20
node-version: '24'
- run: npm install
- run: npm run readme
- uses: stefanzweifel/git-auto-commit-action@v5
+3
View File
@@ -17,3 +17,6 @@ test-mcp-introspection.js
/data
test-screenshot-*.png
test-expand-*.png
app/.webpack-cache
+114
View File
@@ -112,6 +112,120 @@ Both Electron IPC and Socket.io implement the same `EventBusInterface`, allowing
## Security Considerations
### Production Deployment
**CRITICAL**: The following security measures must be implemented for production deployments:
#### 1. HTTPS/TLS Encryption
Always use HTTPS in production to protect credentials and MQTT data in transit:
```bash
# Use a reverse proxy like nginx or Apache with TLS
# Example nginx configuration:
server {
listen 443 ssl http2;
server_name mqtt-explorer.example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
#### 2. Environment Variables for Credentials
**NEVER** use generated credentials in production. Always set secure credentials via environment variables:
```bash
export MQTT_EXPLORER_USERNAME=your_secure_username
export MQTT_EXPLORER_PASSWORD=your_strong_password_min_12_chars
export NODE_ENV=production
yarn start:server
```
#### 3. CORS Configuration
Configure allowed origins instead of using the wildcard (`*`):
```bash
# Single origin
export ALLOWED_ORIGINS=https://mqtt-explorer.example.com
# Multiple origins (comma-separated)
export ALLOWED_ORIGINS=https://app1.example.com,https://app2.example.com
yarn start:server
```
In production with `NODE_ENV=production`, wildcard CORS is automatically disabled for security.
#### 4. Network Security
- Deploy behind a firewall or VPN
- Use IP whitelisting if possible
- Implement network-level rate limiting
- Monitor for suspicious connection patterns
#### 5. File Upload Security
The server implements several protections against malicious file uploads:
- Maximum file size: 16MB (configurable via `MAX_FILE_SIZE` constant)
- Path traversal protection via filename sanitization
- Files stored in isolated directories
- Real path validation to prevent directory escapes
#### 6. Authentication Security
The server implements multiple layers of authentication security:
- **Password Hashing**: bcrypt with 10 rounds
- **Timing Attack Protection**: Constant-time string comparison for usernames
- **Rate Limiting**: Maximum 5 failed attempts per IP per 15 minutes
- **Session Tracking**: Failed attempts are tracked per client IP
- **No Credential Logging**: In production mode, credentials are not logged
#### 7. HTTP Security Headers
The server uses helmet.js to set security headers:
- Content Security Policy (CSP)
- HTTP Strict Transport Security (HSTS) in production
- X-Content-Type-Options: nosniff
- X-Frame-Options: DENY
- X-XSS-Protection
### Security Best Practices
1. **Rotate Credentials Regularly**: Change authentication credentials periodically
2. **Monitor Logs**: Watch for authentication failures and unusual patterns
3. **Keep Dependencies Updated**: Run `yarn audit` regularly
4. **Limit Network Exposure**: Don't expose the server directly to the internet
5. **Use Strong Passwords**: Minimum 12 characters with mixed case, numbers, and symbols
6. **Enable Logging**: Monitor access logs and error logs
7. **Regular Backups**: Back up configuration and certificate data
8. **Principle of Least Privilege**: Run the server with minimal required permissions
### Vulnerability Reporting
If you discover a security vulnerability, please report it via:
- GitHub Security Advisories
- Email to the maintainer
- Do NOT create public issues for security vulnerabilities
### Security Audit Log
- **2024-12**: Initial security review and hardening
- Added helmet.js for HTTP security headers
- Implemented rate limiting for authentication
- Added path traversal protection
- Implemented constant-time comparison for credentials
- Added input validation and size limits
- Removed credential logging in production
- Added configurable CORS origins
- Created comprehensive security test suite
## Security Considerations (Legacy)
1. **HTTPS**: For production, always use HTTPS to encrypt credentials and MQTT data
2. **Authentication**: Keep credentials secure and rotate them regularly
3. **Network**: Ensure the server is on a trusted network or behind a firewall
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:20
FROM node:24
RUN DEBIAN_FRONTEND="noninteractive" apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates nano ffmpeg xvfb git-core tmux locales mosquitto x11vnc
+1 -1
View File
@@ -1,4 +1,4 @@
When redistributing, the attribution page may not be altered or made less accessible without explicit approval.
When distributing, the attribution and donation page may not be altered or made less accessible without explicit approval.
# Creative Commons Attribution-ShareAlike 4.0 International
+143
View File
@@ -0,0 +1,143 @@
# macOS Notarization Setup
This document explains how to set up notarization for macOS builds of MQTT Explorer.
## Overview
macOS notarization is a security feature required by Apple for all software distributed outside the Mac App Store. Starting with macOS 10.15 (Catalina), all software must be notarized to run without warnings on macOS.
## Prerequisites
1. An active Apple Developer account
2. Xcode command line tools installed on the build machine
3. An app-specific password for notarization
## Setup Steps
### 1. Create an App-Specific Password
1. Sign in to [appleid.apple.com](https://appleid.apple.com)
2. Navigate to "Sign-In and Security" section
3. Under "App-Specific Passwords", click "Generate an app-specific password"
4. Enter a descriptive name (e.g., "MQTT Explorer Notarization")
5. Copy the generated password (you won't be able to see it again)
### 2. Find Your Team ID
1. Sign in to [developer.apple.com](https://developer.apple.com/account)
2. Navigate to "Membership Details"
3. Copy your Team ID (a 10-character alphanumeric string)
### 3. Configure GitHub Secrets
Add the following secrets to your GitHub repository:
- `APPLE_ID`: Your Apple ID email address (e.g., `your.email@example.com`)
- `APPLE_APP_SPECIFIC_PASSWORD`: The app-specific password created in step 1
- `APPLE_TEAM_ID`: Your Team ID from step 2
To add secrets:
1. Go to your repository on GitHub
2. Navigate to Settings → Secrets and variables → Actions
3. Click "New repository secret" for each of the above
## How It Works
### Build Configuration
The notarization process is configured in `package.json`:
```json
{
"build": {
"mac": {
"hardenedRuntime": true,
"gatekeeperAssess": false,
"entitlements": "res/entitlements.mac.plist",
"entitlementsInherit": "res/entitlements.mac.inherit.plist"
},
"afterSign": "./dist/scripts/notarize.js"
}
}
```
### Notarization Script
The `scripts/notarize.ts` script handles the notarization process:
1. Checks if the build is for macOS
2. Verifies that required environment variables are set
3. Submits the app to Apple's notarization service
4. Waits for notarization to complete
5. Staples the notarization ticket to the app
### Entitlements
Different entitlements are used for different build types:
- **DMG builds** (regular distribution):
- `res/entitlements.mac.plist` - Main entitlements
- `res/entitlements.mac.inherit.plist` - Inherited entitlements for child processes
- **MAS builds** (Mac App Store):
- `res/entitlements.mas.plist` - App Store specific entitlements
### CI/CD Integration
The GitHub Actions workflow (`platform-builds.yml`) automatically:
1. Builds the macOS app when code is pushed to `release` or `beta` branches
2. Signs the app with the developer certificate
3. Notarizes the app using the configured secrets
4. Publishes the notarized app to GitHub releases
## Troubleshooting
### Notarization Fails
If notarization fails, check:
1. **Credentials**: Ensure all three secrets are correctly set in GitHub
2. **App-specific password**: Verify it hasn't expired or been revoked
3. **Team ID**: Confirm it matches your developer account
4. **Entitlements**: Ensure the entitlements files are valid and appropriate for your app
### Checking Notarization Status
You can check the notarization status of a built app:
```bash
# Check if an app is notarized
spctl -a -vv /path/to/MQTT\ Explorer.app
# Check notarization history
xcrun notarytool history --apple-id your.email@example.com --team-id YOUR_TEAM_ID
```
### Local Testing
To test notarization locally:
```bash
# Set environment variables
export APPLE_ID="your.email@example.com"
export APPLE_APP_SPECIFIC_PASSWORD="your-app-specific-password"
export APPLE_TEAM_ID="YOUR_TEAM_ID"
# Build and notarize
yarn build
yarn package mac
```
## Security Considerations
- Never commit Apple credentials to the repository
- Use app-specific passwords, not your main Apple ID password
- Rotate app-specific passwords periodically
- Limit access to GitHub secrets to trusted maintainers only
## References
- [Apple Notarization Documentation](https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution)
- [electron-builder Code Signing](https://www.electron.build/code-signing)
- [@electron/notarize](https://github.com/electron/notarize)
+4
View File
@@ -116,6 +116,10 @@ yarn ui-test
Create a PR to `release` branch.
There needs to be a "feat: some new feature" or "fix: some bugfix" commit for a new release to be created
### macOS Notarization
macOS builds are automatically notarized during the release process. To set up notarization credentials, see [NOTARIZATION.md](NOTARIZATION.md).
## Create a beta release
Create a PR to `beta` branch. A "feat" or "fix" commit is necessary to create a new version.
+194
View File
@@ -0,0 +1,194 @@
# Security Policy
## Supported Versions
Security updates are provided for the latest release of MQTT Explorer.
| Version | Supported |
| ------- | ------------------ |
| 0.4.x | :white_check_mark: |
| < 0.4 | :x: |
## Reporting a Vulnerability
We take security vulnerabilities seriously. If you discover a security issue, please follow these steps:
### How to Report
1. **DO NOT** create a public GitHub issue for security vulnerabilities
2. Report via one of these channels:
- GitHub Security Advisories (preferred): https://github.com/thomasnordquist/MQTT-Explorer/security/advisories/new
- Email the maintainer directly
3. Include the following information:
- Description of the vulnerability
- Steps to reproduce
- Potential impact
- Any suggested fixes (optional)
### What to Expect
- **Acknowledgment**: We will acknowledge receipt of your report within 48 hours
- **Updates**: We will provide updates on the status of your report within 7 days
- **Fix Timeline**: We aim to release security fixes within 30 days for critical issues
- **Credit**: With your permission, we will credit you in the security advisory and release notes
## Security Features
### Browser Mode Security
MQTT Explorer's browser mode includes several security features:
#### Authentication
- **bcrypt Password Hashing**: All passwords are hashed with bcrypt (10 rounds)
- **Constant-Time Comparison**: Username comparison uses crypto.timingSafeEqual() to prevent timing attacks
- **Environment Variable Configuration**: Credentials can be set via environment variables for production
- **Automatic Credential Generation**: Secure random credentials generated if not provided
#### Rate Limiting
- **Authentication Rate Limiting**: Maximum 5 failed authentication attempts per IP per 15 minutes
- **Per-IP Tracking**: Failed attempts tracked separately for each client IP
- **Automatic Reset**: Rate limit counters automatically reset after 15 minutes
#### HTTP Security Headers (helmet.js)
- **Content Security Policy (CSP)**: Restricts resource loading to prevent XSS attacks
- **HTTP Strict Transport Security (HSTS)**: Enforces HTTPS in production
- **X-Content-Type-Options**: Prevents MIME type sniffing
- **X-Frame-Options**: Prevents clickjacking attacks
- **X-XSS-Protection**: Enables browser XSS protection
#### Input Validation
- **File Size Limits**: Maximum 16MB for file uploads
- **Path Traversal Protection**: All file paths validated and sanitized
- **Filename Sanitization**: Removes path separators, null bytes, and validates against traversal patterns
- **Real Path Validation**: Ensures resolved paths stay within allowed directories
- **Base64 Validation**: All file data properly validated before processing
#### CORS Configuration
- **Configurable Origins**: CORS origins configurable via ALLOWED_ORIGINS environment variable
- **Production Restrictions**: Wildcard CORS automatically disabled in production
- **Credential Support**: CORS configured with credentials: true for authenticated requests
#### Error Handling
- **Generic Error Messages**: Detailed errors only shown in development mode
- **No Information Leakage**: Error messages sanitized to prevent information disclosure
- **Secure Logging**: Sensitive information not logged in production
## Security Best Practices
### For Server Deployment
1. **Always Use HTTPS in Production**
- Use a reverse proxy (nginx, Apache) with TLS certificates
- Never expose the Node.js server directly to the internet
- Use Let's Encrypt for free TLS certificates
2. **Set Strong Credentials**
```bash
export MQTT_EXPLORER_USERNAME=your_secure_username
export MQTT_EXPLORER_PASSWORD=your_strong_password_min_12_chars
export NODE_ENV=production
```
3. **Configure CORS Properly**
```bash
# Single origin
export ALLOWED_ORIGINS=https://mqtt-explorer.example.com
# Multiple origins
export ALLOWED_ORIGINS=https://app1.example.com,https://app2.example.com
```
4. **Network Security**
- Deploy behind a firewall or VPN
- Use IP whitelisting when possible
- Implement network-level rate limiting
- Monitor access logs regularly
5. **Keep Dependencies Updated**
```bash
yarn audit
yarn upgrade-interactive
```
6. **Regular Security Audits**
- Run security tests: `yarn test:security`
- Review access logs for suspicious activity
- Monitor authentication failures
- Check for outdated dependencies
### For MQTT Connections
1. **Use TLS/SSL**: Always connect to MQTT brokers using TLS encryption
2. **Strong Credentials**: Use unique, strong passwords for MQTT authentication
3. **Certificate Validation**: Verify broker certificates in production
4. **Least Privilege**: Connect with minimal required permissions
## Security Testing
The project includes comprehensive security tests:
```bash
# Run all tests including security tests
yarn test
# Run only security tests
npx mocha --require source-map-support/register dist/src/spec/security-tests.spec.js
```
Security tests cover:
- Path traversal attack prevention
- Input validation and sanitization
- Authentication security
- CORS configuration
- Rate limiting
- Error handling
- Data sanitization
## Security Audit History
### December 2024 - Initial Security Review
- Added helmet.js for HTTP security headers
- Implemented rate limiting for authentication
- Added path traversal protection with sanitization
- Implemented constant-time comparison for credentials
- Added input validation and size limits
- Removed credential logging in production
- Added configurable CORS origins
- Created comprehensive security test suite (19 tests)
- Enhanced documentation with security best practices
## Known Limitations
### Browser Mode
- File system access limited to server-side directories
- No native OS dialogs (uses browser file input)
- Session management is stateless (no persistent sessions)
### Desktop Mode (Electron)
- Inherits security model from Electron framework
- IPC communication between renderer and main process
- No network exposure by default
## Recommended Security Tools
- **Dependency Scanning**: Dependabot, Snyk, or npm audit
- **SAST**: SonarQube, ESLint security plugins
- **Container Scanning**: If using Docker deployment
- **TLS Testing**: SSL Labs, testssl.sh
- **Penetration Testing**: OWASP ZAP, Burp Suite
## References
- [OWASP Top 10](https://owasp.org/www-project-top-ten/)
- [Node.js Security Best Practices](https://nodejs.org/en/docs/guides/security/)
- [Express Security Best Practices](https://expressjs.com/en/advanced/best-practice-security.html)
- [helmet.js Documentation](https://helmetjs.github.io/)
- [MQTT Security](https://mqtt.org/mqtt-specification/)
## Contact
For security-related questions or concerns:
- GitHub Security Advisories: https://github.com/thomasnordquist/MQTT-Explorer/security/advisories
- Project Issues (for non-sensitive topics): https://github.com/thomasnordquist/MQTT-Explorer/issues
Thank you for helping keep MQTT Explorer secure!
+58 -55
View File
@@ -6,8 +6,8 @@
"scripts": {
"build": "webpack --mode production",
"dev": "node_modules/.bin/webpack-dev-server --mode development --progress",
"test": "cross-env TS_NODE_PROJECT=test/tsconfig.json yarn mochatest",
"mochatest": "mocha --require ts-node/register --require source-map-support/register --recursive src/*/**/*.spec.ts"
"test": "mocha --require tsx --require source-map-support/register --recursive src/*/**/*.spec.ts",
"mochatest": "mocha --require tsx --require source-map-support/register --recursive src/*/**/*.spec.ts"
},
"engines": {
"node": ">=20"
@@ -15,24 +15,26 @@
"author": "",
"license": "CC-BY-ND-4.0",
"dependencies": {
"@material-ui/core": "4.12",
"@material-ui/icons": "^4",
"@material-ui/lab": "^4.0.0-alpha",
"@material-ui/styles": "4.11",
"@types/react-transition-group": "^4",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^5.18.0",
"@mui/lab": "^5.0.0-alpha.177",
"@mui/material": "^5.18.0",
"@mui/styles": "^6.4.8",
"@types/react-transition-group": "^4.4.11",
"ace-builds": "^1.4.11",
"axios": "^0.28.0",
"compare-versions": "^3.5.0",
"copy-text-to-clipboard": "^2.1.0",
"d3": "^5.9.7",
"d3-shape": "^1.3.5",
"diff": "^4.0.1",
"dot-prop": "^5.0.0",
"axios": "^1.13.2",
"compare-versions": "^6.1.1",
"copy-text-to-clipboard": "^3.2.0",
"d3": "^7.9.0",
"d3-shape": "^3.2.0",
"diff": "^7.0.0",
"dot-prop": "^5.3.0",
"events": "^3.3.0",
"get-value": "^3.0.1",
"immutable": "^4.0.0-rc.12",
"immutable": "^4.3.7",
"in-viewport": "^3.6.0",
"js-base64": "^2.5.1",
"js-base64": "^3.7.8",
"json-to-ast": "^2.1.0",
"lodash.debounce": "^4.0.8",
"lodash.throttle": "^4.1.1",
@@ -41,55 +43,56 @@
"os-browserify": "^0.3.0",
"parse-duration": "^0.1.1",
"path-browserify": "^1.0.1",
"prismjs": "^1.15.0",
"react": "^16.11",
"react-ace": "^8",
"react-dom": "^16.7.0",
"react-redux": "^7.0.3",
"react-resize-detector": "^4.1.4",
"react-split-pane": "^0.1.85",
"react-transition-group": "^4",
"react-vis": "^1.11.6",
"redux": "^4.0.1",
"redux-batched-actions": "0.5",
"redux-thunk": "^2.3.0",
"prismjs": "^1.29.0",
"react": "^18.3.1",
"react-ace": "^12.0.0",
"react-dom": "^18.3.1",
"react-redux": "^9.2.0",
"react-resize-detector": "^11.0.1",
"react-split-pane": "^0.1.92",
"react-transition-group": "^4.4.5",
"react-vis": "^1.12.1",
"redux": "^5.0.1",
"redux-batched-actions": "^0.5.0",
"redux-thunk": "^3.1.0",
"sha1": "^1.1.1",
"socket.io-client": "^4.8.1",
"url": "^0.11.4",
"uuid": "7"
"uuid": "^11.0.0"
},
"devDependencies": {
"@babel/runtime": "^7.17.2",
"@types/d3": "^5.7.2",
"@types/diff": "^4.0.1",
"@types/get-value": "^3.0.1",
"@types/node": "^12.7.8",
"@types/prismjs": "^1.9.1",
"@types/react": "^16.9.4",
"@types/react-dom": "^16.0.11",
"@types/react-redux": "^7.0.9",
"@types/react-resize-detector": "^4.0.1",
"@babel/runtime": "^7.28.4",
"@types/d3": "^7.4.3",
"@types/diff": "^7.0.0",
"@types/get-value": "^3.0.5",
"@types/lodash.debounce": "^4.0.9",
"@types/node": "^25.0.3",
"@types/prismjs": "^1.26.5",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@types/react-redux": "^7.1.34",
"@types/react-resize-detector": "^4.0.3",
"@types/sha1": "^1.1.1",
"@types/socket.io-client": "^3.0.0",
"@types/uuid": "^7.0.2",
"@types/vis": "^4.21.9",
"chai": "^4.2.0",
"cross-env": "^7.0.2",
"css-loader": "^3.0.0",
"@types/uuid": "^11.0.0",
"@types/vis": "^4.21.24",
"chai": "^4.5.0",
"cross-env": "^7.0.3",
"css-loader": "^7.1.2",
"file-loader": "^6.2.0",
"html-webpack-plugin": "^5.5.0",
"html-webpack-plugin": "^5.6.3",
"lodash": "^4.17.21",
"mocha": "^10.4.0",
"moment": "^2.29.1",
"node-loader": "^0.6.0",
"source-map-loader": "^0.2.4",
"style-loader": "^1",
"mocha": "^10.8.2",
"moment": "^2.30.1",
"node-loader": "^2.0.0",
"source-map-loader": "^5.0.0",
"style-loader": "^4.0.0",
"ts-loader": "^9.5.1",
"typescript": "^4.5.5",
"webpack": "^5.91.0",
"webpack-bundle-analyzer": "^4.5.0",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^5.0.4"
"typescript": "^5.9.3",
"webpack": "^5.98.0",
"webpack-bundle-analyzer": "^4.10.2",
"webpack-cli": "^6.0.1",
"webpack-dev-server": "^5.2.0"
},
"peerDependencies": {
"electron": "^39"
+6
View File
@@ -147,6 +147,12 @@ export const toggleCertificateSettings = (): Action => ({
type: ActionTypes.CONNECTION_MANAGER_TOGGLE_CERTIFICATE_SETTINGS,
})
export const moveConnection = (connectionId: string, direction: 'up' | 'down'): Action => ({
connectionId,
direction,
type: ActionTypes.CONNECTION_MANAGER_MOVE_CONNECTION,
})
export const deleteConnection = (connectionId: string) => (dispatch: Dispatch<any>, getState: () => AppState) => {
const connectionIds = Object.keys(getState().connectionManager.connections)
const connectionIdLocation = connectionIds.indexOf(connectionId)
+2 -2
View File
@@ -15,7 +15,7 @@ export const setTopic = (topic?: string): Action => {
}
export const openFile =
(encoding: 'utf8' = 'utf8') =>
(encoding: BufferEncoding = 'utf8') =>
async (dispatch: Dispatch<any>, getState: () => AppState) => {
try {
const file = await getFileContent(encoding)
@@ -31,7 +31,7 @@ type FileParameters = {
name: string
data: string
}
async function getFileContent(encoding: string): Promise<FileParameters | undefined> {
async function getFileContent(encoding: BufferEncoding): Promise<FileParameters | undefined> {
const rejectReasons = {
noFileSelected: 'No file selected',
errorReadingFile: 'Error reading file',
+1 -1
View File
@@ -7,7 +7,7 @@ import { batchActions } from 'redux-batched-actions'
import { globalActions } from './'
import { setTopic } from './Publish'
import { TopicViewModel } from '../model/TopicViewModel'
const debounce = require('lodash.debounce')
import debounce from 'lodash.debounce'
export { clearTopic } from './clearTopic'
export { moveSelectionUpOrDownwards, moveInward, moveOutward } from './visibleTreeTraversal'
+18 -1
View File
@@ -68,6 +68,17 @@ let migrations: Migration[] = [
}
},
},
// Add order field for connection ordering
{
from: 1,
apply: (connection: ConnectionOptions): ConnectionOptions => {
if (connection.order !== undefined) {
return connection
}
// Order will be assigned during migration based on current position
return connection
},
},
]
const connectionMigrator = new ConfigMigrator(migrations)
@@ -80,8 +91,14 @@ function isMigrationNecessary(connections: ConnectionDictionary): boolean {
function applyMigrations(connections: ConnectionDictionary): ConnectionDictionary {
let newConnectionDictionary: ConnectionDictionary = {}
Object.keys(connections).forEach(key => {
const connectionKeys = Object.keys(connections)
connectionKeys.forEach((key, index) => {
let newConnection = connectionMigrator.applyMigrations(connections[key]) as any
// If the migration didn't assign an order, assign one based on current position
if (newConnection.order === undefined) {
newConnection.order = index
}
newConnectionDictionary[newConnection.id] = newConnection
})
+115
View File
@@ -0,0 +1,115 @@
// Browser-specific EventBus implementation using Socket.io
// This file contains the socket.io-client dependency which belongs in the app layer
import io, { Socket } from 'socket.io-client'
import { SocketIOClientEventBus } from '../../events/EventSystem/SocketIOClientEventBus'
import { Rpc } from '../../events/EventSystem/Rpc'
// Get auth from sessionStorage or use empty (will show login dialog)
let username = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('mqtt-explorer-username') || '' : ''
let password = typeof sessionStorage !== 'undefined' ? sessionStorage.getItem('mqtt-explorer-password') || '' : ''
// Connect to the server (same origin in browser mode)
const socket: Socket = io({
auth: {
username,
password,
},
reconnection: true,
reconnectionDelay: 1000,
reconnectionDelayMax: 5000,
reconnectionAttempts: Infinity,
transports: ['websocket', 'polling'],
autoConnect: false, // Don't auto-connect, we'll connect manually after checking credentials
})
// Handle connection errors
socket.on('connect_error', (error) => {
console.error('Socket connection error:', error.message)
// Check if it's an authentication error
if (error.message.includes('Invalid credentials') ||
error.message.includes('Authentication required') ||
error.message.includes('Too many')) {
// Clear invalid credentials from sessionStorage
if (typeof sessionStorage !== 'undefined') {
sessionStorage.removeItem('mqtt-explorer-username')
sessionStorage.removeItem('mqtt-explorer-password')
}
// Dispatch custom event that BrowserAuthWrapper can listen to
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('mqtt-auth-error', {
detail: { message: error.message }
}))
}
}
})
socket.on('disconnect', (reason) => {
console.log('Socket disconnected:', reason)
})
socket.on('connect', () => {
console.log('Socket connected successfully')
// Dispatch custom event that BrowserAuthWrapper can listen to
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('mqtt-auth-success', {
detail: { message: 'Authentication successful' }
}))
}
})
/**
* Update socket authentication credentials and attempt to reconnect
* @param newUsername New username
* @param newPassword New password
*/
export function updateSocketAuth(newUsername: string, newPassword: string) {
username = newUsername
password = newPassword
// Update socket auth
socket.auth = {
username: newUsername,
password: newPassword,
}
// Store in sessionStorage
if (typeof sessionStorage !== 'undefined') {
sessionStorage.setItem('mqtt-explorer-username', newUsername)
sessionStorage.setItem('mqtt-explorer-password', newPassword)
}
// Disconnect if connected, then reconnect with new credentials
if (socket.connected) {
socket.disconnect()
}
socket.connect()
}
/**
* Connect the socket (used on initial page load)
*/
export function connectSocket() {
if (!socket.connected) {
socket.connect()
}
}
export const rendererEvents = new SocketIOClientEventBus(socket)
export const rendererRpc = new Rpc(rendererEvents)
// Export socket instance for error monitoring
export const browserSocket = socket
// In browser mode, the backend is on the server
// For compatibility, export same instances (renderer communicates with server backend via socket)
export const backendEvents = rendererEvents
export const backendRpc = rendererRpc
// Re-export all events from the events module so imports work correctly
export * from '../../events/Events'
export * from '../../events/EventsV2'
export * from '../../events/EventSystem/EventDispatcher'
export * from '../../events/EventSystem/EventBusInterface'
+3 -2
View File
@@ -1,6 +1,6 @@
import ConfirmationDialog from './ConfirmationDialog'
import ConnectionSetup from './ConnectionSetup/ConnectionSetup'
import CssBaseline from '@material-ui/core/CssBaseline'
import CssBaseline from '@mui/material/CssBaseline'
import ErrorBoundary from './ErrorBoundary'
import Notification from './Layout/Notification'
import React from 'react'
@@ -11,7 +11,8 @@ import { bindActionCreators } from 'redux'
import { ConfirmationRequest } from '../reducers/Global'
import { connect } from 'react-redux'
import { globalActions, settingsActions } from '../actions'
import { Theme, withStyles } from '@material-ui/core/styles'
import { Theme } from '@mui/material/styles'
import { withStyles } from '@mui/styles'
;(window as any).global = window
const Settings = React.lazy(() => import('./SettingsDrawer/Settings'))
+73 -20
View File
@@ -1,17 +1,18 @@
import * as React from 'react'
import { LoginDialog } from './LoginDialog'
import { updateSocketAuth, connectSocket } from '../browserEventBus'
import { isBrowserMode } from '../utils/browserMode'
interface BrowserAuthWrapperProps {
children: React.ReactNode
}
const isBrowserMode =
typeof window !== 'undefined' && (typeof process === 'undefined' || process.env?.BROWSER_MODE === 'true')
export function BrowserAuthWrapper(props: BrowserAuthWrapperProps) {
const [isAuthenticated, setIsAuthenticated] = React.useState(false)
const [loginError, setLoginError] = React.useState<string | undefined>()
const [showLogin, setShowLogin] = React.useState(false)
const [showLogin, setShowLogin] = React.useState(isBrowserMode) // Show login initially in browser mode
const [waitTimeSeconds, setWaitTimeSeconds] = React.useState<number | undefined>()
const [isConnecting, setIsConnecting] = React.useState(false)
React.useEffect(() => {
if (!isBrowserMode) {
@@ -20,34 +21,86 @@ export function BrowserAuthWrapper(props: BrowserAuthWrapperProps) {
return
}
// Listen for successful authentication from socket
const handleAuthSuccess = (event: CustomEvent) => {
console.log('Authentication successful')
setIsAuthenticated(true)
setShowLogin(false)
setLoginError(undefined)
setWaitTimeSeconds(undefined)
setIsConnecting(false)
}
// Listen for authentication errors from socket
const handleAuthError = (event: CustomEvent) => {
const errorMessage = event.detail?.message || 'Authentication failed'
console.error('Authentication error:', errorMessage)
// Clear authentication state
setIsAuthenticated(false)
setShowLogin(true)
setIsConnecting(false)
// Extract wait time from error message (e.g., "Please wait 30 seconds")
const waitTimeMatch = errorMessage.match(/(\d+)\s+seconds?/)
if (waitTimeMatch) {
const seconds = parseInt(waitTimeMatch[1], 10)
// Add a few seconds margin to the countdown
setWaitTimeSeconds(seconds + 3)
} else {
setWaitTimeSeconds(undefined)
}
// Set user-friendly error message based on error type
// Error messages from server already include wait times
if (errorMessage.includes('Too many failed authentication attempts')) {
setLoginError(errorMessage)
} else if (errorMessage.includes('Invalid credentials')) {
setLoginError(errorMessage)
} else if (errorMessage.includes('Authentication required')) {
setLoginError('Please enter your username and password.')
setWaitTimeSeconds(undefined)
} else {
setLoginError('Authentication failed. Please try again.')
setWaitTimeSeconds(undefined)
}
}
window.addEventListener('mqtt-auth-success', handleAuthSuccess as EventListener)
window.addEventListener('mqtt-auth-error', handleAuthError as EventListener)
// Check if already authenticated
const username = sessionStorage.getItem('mqtt-explorer-username')
const password = sessionStorage.getItem('mqtt-explorer-password')
if (username && password) {
// Try to use stored credentials
setIsAuthenticated(true)
// Credentials exist, try to connect with them
setIsConnecting(true)
connectSocket()
} else {
// Show login dialog
// No credentials, show login dialog
setShowLogin(true)
}
return () => {
window.removeEventListener('mqtt-auth-success', handleAuthSuccess as EventListener)
window.removeEventListener('mqtt-auth-error', handleAuthError as EventListener)
}
}, [])
const handleLogin = async (username: string, password: string) => {
const handleLogin = (username: string, password: string) => {
try {
// Store credentials in session storage
sessionStorage.setItem('mqtt-explorer-username', username)
sessionStorage.setItem('mqtt-explorer-password', password)
// The socket will use these credentials on next connection
setIsAuthenticated(true)
setShowLogin(false)
// Clear any previous error
setLoginError(undefined)
// Reload to reinitialize socket with new auth
window.location.reload()
setWaitTimeSeconds(undefined)
setIsConnecting(true)
// Update socket auth and reconnect (no page reload needed)
updateSocketAuth(username, password)
} catch (error) {
setLoginError('Login failed. Please check your credentials.')
console.error('Failed to update socket auth:', error)
setLoginError('Failed to connect. Please try again.')
setIsConnecting(false)
}
}
@@ -57,7 +110,7 @@ export function BrowserAuthWrapper(props: BrowserAuthWrapperProps) {
}
if (!isAuthenticated) {
return <LoginDialog open={showLogin} onLogin={handleLogin} error={loginError} />
return <LoginDialog open={showLogin} onLogin={handleLogin} error={loginError} waitTimeSeconds={waitTimeSeconds} />
}
return <>{props.children}</>
+70 -75
View File
@@ -1,14 +1,13 @@
import DateFormatter from '../helper/DateFormatter'
import NoData from './NoData'
import NumberFormatter from '../helper/NumberFormatter'
import React, { memo, useCallback } from 'react'
import React, { memo, useCallback, useRef, useEffect } from 'react'
import TooltipComponent from './TooltipComponent'
import { default as ReactResizeDetector } from 'react-resize-detector'
import { emphasize } from '@material-ui/core/styles'
import { useResizeDetector } from 'react-resize-detector'
import { emphasize, useTheme } from '@mui/material/styles'
import { mapCurveType } from './mapCurveType'
import { PlotCurveTypes } from '../../reducers/Charts'
import { Point, Tooltip } from './Model'
import { Theme, withTheme } from '@material-ui/core'
import { useCustomXDomain } from './effects/useCustomXDomain'
import { useCustomYDomain } from './effects/useCustomYDomain'
import 'react-vis/dist/style.css'
@@ -17,89 +16,85 @@ const abbreviate = require('number-abbreviate')
export interface Props {
data: Array<{ x: number; y: number }>
theme: Theme
interpolation?: PlotCurveTypes
range?: [number?, number?]
timeRangeStart?: number
color?: string
}
export default withTheme(
memo((props: Props) => {
const [width, setWidth] = React.useState(300)
const [tooltip, setTooltip] = React.useState<Tooltip | undefined>()
const detectResize = React.useCallback(newWidth => setWidth(newWidth), [])
export default memo((props: Props) => {
const theme = useTheme()
const [tooltip, setTooltip] = React.useState<Tooltip | undefined>()
const { width = 300, ref } = useResizeDetector()
const hintFormatter = React.useCallback(
(point: any) => [
{ title: <b>Time</b>, value: <DateFormatter timeFirst={true} date={new Date(point.x)} /> },
{ title: <b>Value</b>, value: <NumberFormatter value={point.y} /> },
{ title: <b>Raw</b>, value: <span>{point.y}</span> },
],
[]
)
const hintFormatter = React.useCallback(
(point: any) => [
{ title: <b>Time</b>, value: <DateFormatter timeFirst={true} date={new Date(point.x)} /> },
{ title: <b>Value</b>, value: <NumberFormatter value={point.y} /> },
{ title: <b>Raw</b>, value: <span>{point.y}</span> },
],
[]
)
const onMouseLeave = React.useCallback(() => {
setTooltip(undefined)
}, [])
const onMouseLeave = React.useCallback(() => {
setTooltip(undefined)
}, [])
const showTooltip = React.useCallback((point: Point, something: { event: MouseEvent }) => {
if (!something) {
return
}
setTooltip({ point, value: hintFormatter(point), element: something.event.target as any })
}, [])
const showTooltip = React.useCallback((point: Point, something: { event: MouseEvent }) => {
if (!something) {
return
}
setTooltip({ point, value: hintFormatter(point), element: something.event.target as any })
}, [])
const paletteColor =
props.theme.palette.type === 'light' ? props.theme.palette.secondary.dark : props.theme.palette.primary.light
const color = props.color ? props.color : paletteColor
const paletteColor =
theme.palette.mode === 'light' ? theme.palette.secondary.dark : theme.palette.primary.light
const color = props.color ? props.color : paletteColor
const highlightSelectedPoint = useCallback(
(point: Point) => {
const highlight = tooltip && tooltip.point.x === point.x && tooltip.point.y === point.y
return highlight ? emphasize(color, 0.8) : color
},
[tooltip, color]
)
const highlightSelectedPoint = useCallback(
(point: Point) => {
const highlight = tooltip && tooltip.point.x === point.x && tooltip.point.y === point.y
return highlight ? emphasize(color, 0.8) : color
},
[tooltip, color]
)
const formatYAxis = useCallback((num: number) => abbreviate(num), [])
const formatYAxis = useCallback((num: number) => abbreviate(num), [])
const xDomain = useCustomXDomain(props)
const yDomain = useCustomYDomain(props)
const xDomain = useCustomXDomain(props)
const yDomain = useCustomYDomain(props)
const data = props.data
const hasData = data.length > 0
const dummyDomain = [-1, 1]
const dummyData = [{ x: -2, y: -2 }]
return (
<div>
<div style={{ height: '150px', width: '100%', position: 'relative' }}>
{data.length === 0 ? <NoData /> : null}
<XYPlot
width={width}
height={180}
yDomain={hasData ? yDomain : dummyDomain}
xDomain={hasData ? xDomain : dummyDomain}
onMouseLeave={onMouseLeave}
>
<HorizontalGridLines />
<YAxis width={45} tickFormat={formatYAxis} />
<LineMarkSeries
color={color}
colorType="literal"
getColor={highlightSelectedPoint}
onValueMouseOver={showTooltip}
size={3}
data={hasData ? data : dummyData}
curve={mapCurveType(props.interpolation)}
/>
<Hint value={{ x: 0, y: 0 }} style={{ pointerEvents: 'none' }}>
<TooltipComponent tooltip={tooltip} theme={props.theme} />
</Hint>
</XYPlot>
<ReactResizeDetector handleWidth={true} onResize={detectResize} />
</div>
const data = props.data
const hasData = data.length > 0
const dummyDomain = [-1, 1]
const dummyData = [{ x: -2, y: -2 }]
return (
<div>
<div ref={ref} style={{ height: '150px', width: '100%', position: 'relative' }}>
{data.length === 0 ? <NoData /> : null}
<XYPlot
width={width || 300}
height={180}
yDomain={hasData ? yDomain : dummyDomain}
xDomain={hasData ? xDomain : dummyDomain}
onMouseLeave={onMouseLeave}
>
<HorizontalGridLines />
<YAxis width={45} tickFormat={formatYAxis} />
<LineMarkSeries
color={color}
colorType="literal"
getColor={highlightSelectedPoint}
onValueMouseOver={showTooltip}
size={3}
data={hasData ? data : dummyData}
curve={mapCurveType(props.interpolation)}
/>
<Hint value={{ x: 0, y: 0 }} style={{ pointerEvents: 'none' }}>
<TooltipComponent tooltip={tooltip} />
</Hint>
</XYPlot>
</div>
)
})
)
</div>
)
})
+1 -1
View File
@@ -1,5 +1,5 @@
import React, { memo } from 'react'
import { Typography } from '@material-ui/core'
import { Typography } from '@mui/material'
function NoData() {
return (
@@ -1,9 +1,10 @@
import React, { memo } from 'react'
import { fade } from '@material-ui/core/styles'
import { Fade, Grow, Paper, Popper, Theme, Typography, withTheme } from '@material-ui/core'
import { alpha as fade, useTheme } from '@mui/material/styles'
import { Fade, Grow, Paper, Popper, Typography } from '@mui/material'
import { Tooltip } from './Model'
function TooltipComponent(props: { tooltip?: Tooltip; theme: Theme }) {
function TooltipComponent(props: { tooltip?: Tooltip }) {
const theme = useTheme()
const { tooltip } = props
return (
<Popper
@@ -26,9 +27,9 @@ function TooltipComponent(props: { tooltip?: Tooltip; theme: Theme }) {
padding: '4px',
marginTop: '-12px',
backgroundColor: fade(
props.theme.palette.type === 'light'
? props.theme.palette.background.paper
: props.theme.palette.background.default,
theme.palette.mode === 'light'
? theme.palette.background.paper
: theme.palette.background.default,
0.7
),
}}
@@ -56,4 +57,4 @@ function TooltipComponent(props: { tooltip?: Tooltip; theme: Theme }) {
)
}
export default withTheme(memo(TooltipComponent))
export default memo(TooltipComponent)
@@ -1,7 +1,7 @@
import React, { useRef } from 'react'
import Play from '@material-ui/icons/PlayArrow'
import Pause from '@material-ui/icons/PauseCircleFilled'
import Clear from '@material-ui/icons/Clear'
import Play from '@mui/icons-material/PlayArrow'
import Pause from '@mui/icons-material/PauseCircleFilled'
import Clear from '@mui/icons-material/Clear'
import CustomIconButton from '../helper/CustomIconButton'
import { ChartParameters } from '../../reducers/Charts'
import { SettingsButton } from './ChartSettings/SettingsButton'
@@ -3,7 +3,7 @@ import { bindActionCreators } from 'redux'
import { chartActions } from '../../../actions'
import { ChartParameters } from '../../../reducers/Charts'
import { connect } from 'react-redux'
import { Menu, MenuItem } from '@material-ui/core'
import { Menu, MenuItem } from '@mui/material'
import { colors as createColors } from './colors'
function chartParametersForColor(chart: ChartParameters, color?: string) {
@@ -4,7 +4,7 @@ import { bindActionCreators } from 'redux'
import { chartActions } from '../../../actions'
import { ChartParameters, PlotCurveTypes } from '../../../reducers/Charts'
import { connect } from 'react-redux'
import { Menu, MenuItem, Typography } from '@material-ui/core'
import { Menu, MenuItem, Typography } from '@mui/material'
function chartParametersForAction(chart: ChartParameters, action: string) {
return {
@@ -39,7 +39,12 @@ function InterpolationSettings(props: {
const menuItems = React.useMemo(() => {
return curves.map(curve => (
<MenuItem key={curve} onClick={callbacks[curve]} selected={props.chart.interpolation === curve}>
<MenuItem
key={curve}
onClick={callbacks[curve]}
selected={props.chart.interpolation === curve}
data-menu-item={curve.replace(/_/g, ' ')}
>
<Typography variant="inherit">{curve.replace(/_/g, ' ')}</Typography>
</MenuItem>
))
@@ -1,10 +1,10 @@
import * as React from 'react'
import ArrowUpward from '@material-ui/icons/ArrowUpward'
import ArrowUpward from '@mui/icons-material/ArrowUpward'
import { bindActionCreators } from 'redux'
import { chartActions } from '../../../actions'
import { ChartParameters } from '../../../reducers/Charts'
import { connect } from 'react-redux'
import { MenuItem, Typography, ListItemIcon } from '@material-ui/core'
import { MenuItem, Typography, ListItemIcon } from '@mui/material'
function MoveUp(props: { actions: { chart: typeof chartActions }; chart: ChartParameters; close: () => void }) {
const moveUp = React.useCallback(() => {
@@ -1,6 +1,6 @@
import React, { useCallback, useState, ChangeEvent, MouseEvent, useRef, useEffect, useMemo } from 'react'
import { ChartParameters } from '../../../reducers/Charts'
import { Menu, TextField, Typography } from '@material-ui/core'
import { Menu, TextField, Typography } from '@mui/material'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { chartActions } from '../../../actions'
@@ -1,7 +1,7 @@
import * as React from 'react'
import ChartSettings from '.'
import CustomIconButton from '../../helper/CustomIconButton'
import MoreVertIcon from '@material-ui/icons/Settings'
import MoreVertIcon from '@mui/icons-material/Settings'
import { ChartParameters } from '../../../reducers/Charts'
export function SettingsButton(props: {
@@ -1,6 +1,6 @@
import React, { memo } from 'react'
import { ChartParameters } from '../../../reducers/Charts'
import { Menu, MenuItem, TextField, Typography } from '@material-ui/core'
import { Menu, MenuItem, TextField, Typography } from '@mui/material'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { chartActions } from '../../../actions'
@@ -1,6 +1,6 @@
import React, { ChangeEvent, MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { bindActionCreators } from 'redux'
import { Button, Menu, TextField, Typography } from '@material-ui/core'
import { Button, Menu, TextField, Typography } from '@mui/material'
import { chartActions } from '../../../actions'
import { ChartParameters } from '../../../reducers/Charts'
import { connect } from 'react-redux'
@@ -12,7 +12,7 @@ import {
yellow,
brown,
blueGrey,
} from '@material-ui/core/colors'
} from '@mui/material/colors'
export function colors() {
function colorToInt(color: string): [number, number, number] {
@@ -1,17 +1,17 @@
import BarChart from '@material-ui/icons/BarChart'
import Clear from '@material-ui/icons/Refresh'
import ColorLens from '@material-ui/icons/ColorLens'
import BarChart from '@mui/icons-material/BarChart'
import Clear from '@mui/icons-material/Refresh'
import ColorLens from '@mui/icons-material/ColorLens'
import ColorSettings from './ColorSettings'
import InterpolationSettings from './InterpolationSettings'
import MoveUp from './MoveUp'
import MultilineChart from '@material-ui/icons/MultilineChart'
import MultilineChart from '@mui/icons-material/MultilineChart'
import RangeSettings from './RangeSettings'
import React, { memo } from 'react'
import Size from './Size'
import Sort from '@material-ui/icons/Sort'
import Sort from '@mui/icons-material/Sort'
import TimeRangeSettings from './TimeRangeSettings'
import { ChartParameters } from '../../../reducers/Charts'
import { Menu, MenuItem, ListItemIcon, Typography } from '@material-ui/core'
import { Menu, MenuItem, ListItemIcon, Typography } from '@mui/material'
function ChartSettings(props: {
open: boolean
@@ -65,37 +65,37 @@ function ChartSettings(props: {
return (
<span>
<Menu id="long-menu" anchorEl={props.anchorEl.current} open={props.open} onClose={props.close}>
<MenuItem key="range" onClick={toggleRange}>
<MenuItem key="range" onClick={toggleRange} data-menu-item="Y-Axis range (Values)">
<ListItemIcon>
<BarChart />
</ListItemIcon>
<Typography variant="inherit">Y-Axis range (Values)</Typography>
</MenuItem>
<MenuItem key="timeRange" onClick={toggleTimeRange}>
<MenuItem key="timeRange" onClick={toggleTimeRange} data-menu-item="X-Axis range (Time)">
<ListItemIcon>
<BarChart />
</ListItemIcon>
<Typography variant="inherit">X-Axis range (Time)</Typography>
</MenuItem>
<MenuItem key="interpolation" onClick={toggleInterpolation}>
<MenuItem key="interpolation" onClick={toggleInterpolation} data-menu-item="Curve interpolation">
<ListItemIcon>
<MultilineChart />
</ListItemIcon>
<Typography variant="inherit">Curve interpolation</Typography>
</MenuItem>
<MenuItem key="size" onClick={toggleSize}>
<MenuItem key="size" onClick={toggleSize} data-menu-item="Size">
<ListItemIcon>
<Sort />
</ListItemIcon>
<Typography variant="inherit">Size</Typography>
</MenuItem>
<MenuItem key="color" onClick={toggleColor}>
<MenuItem key="color" onClick={toggleColor} data-menu-item="Color">
<ListItemIcon>
<ColorLens />
</ListItemIcon>
<Typography variant="inherit">Color</Typography>
</MenuItem>
<MenuItem key="clear" onClick={props.resetDataAction}>
<MenuItem key="clear" onClick={props.resetDataAction} data-menu-item="Clear data">
<ListItemIcon>
<Clear />
</ListItemIcon>
+3 -1
View File
@@ -1,6 +1,8 @@
import * as React from 'react'
import { ChartParameters } from '../../reducers/Charts'
import { Typography, Theme, withStyles } from '@material-ui/core'
import { Typography } from '@mui/material'
import { withStyles } from '@mui/styles'
import { Theme } from '@mui/material/styles'
function ChartTitle(props: { parameters: ChartParameters; classes: any }) {
const { classes, parameters } = props
+1 -1
View File
@@ -7,7 +7,7 @@ import { ChartActions } from './ChartActions'
import { chartActions } from '../../actions'
import { ChartParameters } from '../../reducers/Charts'
import { connect } from 'react-redux'
import { Paper } from '@material-ui/core'
import { Paper } from '@mui/material'
const throttle = require('lodash.throttle')
class ClearableMessageBuffer extends q.RingBuffer<q.Message> {
+5 -3
View File
@@ -1,13 +1,15 @@
import * as q from '../../../../backend/src/Model'
import * as React from 'react'
import ShowChart from '@material-ui/icons/ShowChart'
import ShowChart from '@mui/icons-material/ShowChart'
import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux'
import { chartActions } from '../../actions'
import { ChartParameters } from '../../reducers/Charts'
import { ChartWithTreeNode } from './ChartWithTreeNode'
import { connect } from 'react-redux'
import { Grid, Theme, Typography, withStyles } from '@material-ui/core'
import { Grid, Typography } from '@mui/material'
import { withStyles } from '@mui/styles'
import { Theme } from '@mui/material/styles'
import { List } from 'immutable'
const { TransitionGroup, CSSTransition } = require('react-transition-group/esm')
@@ -126,4 +128,4 @@ const styles = (theme: Theme) => ({
},
})
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ChartPanel))
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ChartPanel) as any)
+1 -1
View File
@@ -1,6 +1,6 @@
import React, { useRef, useCallback, memo } from 'react'
import { ConfirmationRequest } from '../reducers/Global'
import { Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, Button } from '@material-ui/core'
import { Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, Button } from '@mui/material'
import { KeyCodes } from '../utils/KeyCodes'
function ConfirmationDialog(props: { confirmationRequests: Array<ConfirmationRequest> }) {
@@ -1,17 +1,19 @@
import * as React from 'react'
import { useState, useCallback, memo } from 'react'
import Add from '@material-ui/icons/Add'
import Lock from '@material-ui/icons/Lock'
import Undo from '@material-ui/icons/Undo'
import Add from '@mui/icons-material/Add'
import Lock from '@mui/icons-material/Lock'
import Undo from '@mui/icons-material/Undo'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { connectionManagerActions } from '../../actions'
import { ConnectionOptions } from '../../model/ConnectionOptions'
import { Theme, withStyles } from '@material-ui/core/styles'
import { Button, Grid, TextField, Tooltip } from '@material-ui/core'
import { Theme } from '@mui/material/styles'
import { withStyles } from '@mui/styles'
import { Button, Grid, TextField, Tooltip } from '@mui/material'
import { QosSelect } from '../QosSelect'
import { QoS } from '../../../../backend/src/DataSource/MqttSource'
import Subscriptions from './Subscriptions'
const SubscriptionsAny = Subscriptions as any
interface Props {
connection: ConnectionOptions
@@ -63,12 +65,13 @@ const ConnectionSettings = memo(function ConnectionSettings(props: Props) {
color="secondary"
onClick={() => props.managerActions.addSubscription({ topic, qos }, props.connection.id)}
variant="contained"
data-testid="add-subscription-button"
>
<Add /> Add
</Button>
</Grid>
<Grid item={true} xs={12} style={{ padding: 0 }}>
<Subscriptions connection={props.connection} />
<SubscriptionsAny connection={props.connection} />
</Grid>
<Grid item={true} xs={7} className={classes.gridPadding}>
<TextField
@@ -97,6 +100,7 @@ const ConnectionSettings = memo(function ConnectionSettings(props: Props) {
variant="contained"
className={classes.button}
onClick={props.managerActions.toggleAdvancedSettings}
data-testid="back-button"
>
<Undo /> Back
</Button>
@@ -129,4 +133,4 @@ const styles = (theme: Theme) => ({
},
})
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(ConnectionSettings))
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(ConnectionSettings) as any)
@@ -1,13 +1,13 @@
import * as React from 'react'
import ClearAdornment from '../helper/ClearAdornment'
import Lock from '@material-ui/icons/Lock'
import Lock from '@mui/icons-material/Lock'
import { bindActionCreators } from 'redux'
import { Button, Theme, Tooltip, Typography } from '@material-ui/core'
import { Button, Theme, Tooltip, Typography } from '@mui/material'
import { CertificateParameters, ConnectionOptions } from '../../model/ConnectionOptions'
import { CertificateTypes } from '../../actions/ConnectionManager'
import { connect } from 'react-redux'
import { connectionManagerActions } from '../../actions'
import { withStyles } from '@material-ui/styles'
import { withStyles } from '@mui/styles'
import { rendererRpc } from '../../../../events'
import { RpcEvents } from '../../../../events/EventsV2'
@@ -129,7 +129,7 @@ const styles = (theme: Theme) => ({
overflow: 'hidden' as 'hidden',
whiteSpace: 'nowrap' as 'nowrap',
textOverflow: 'ellipsis' as 'ellipsis',
color: theme.palette.text.hint,
color: theme.palette.text.secondary,
},
button: {
marginTop: theme.spacing(3),
@@ -137,4 +137,4 @@ const styles = (theme: Theme) => ({
},
})
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(BrowserCertificateFileSelection))
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(BrowserCertificateFileSelection) as any)
@@ -1,13 +1,13 @@
import * as React from 'react'
import ClearAdornment from '../helper/ClearAdornment'
import Lock from '@material-ui/icons/Lock'
import Lock from '@mui/icons-material/Lock'
import { bindActionCreators } from 'redux'
import { Button, Theme, Tooltip, Typography } from '@material-ui/core'
import { Button, Theme, Tooltip, Typography } from '@mui/material'
import { CertificateParameters, ConnectionOptions } from '../../model/ConnectionOptions'
import { CertificateTypes } from '../../actions/ConnectionManager'
import { connect } from 'react-redux'
import { connectionManagerActions } from '../../actions'
import { withStyles } from '@material-ui/styles'
import { withStyles } from '@mui/styles'
function CertificateFileSelection(props: {
certificateType: CertificateTypes
@@ -71,7 +71,7 @@ const styles = (theme: Theme) => ({
overflow: 'hidden' as 'hidden',
whiteSpace: 'nowrap' as 'nowrap',
textOverflow: 'ellipsis' as 'ellipsis',
color: theme.palette.text.hint,
color: theme.palette.text.secondary,
},
button: {
marginTop: theme.spacing(3),
@@ -79,4 +79,4 @@ const styles = (theme: Theme) => ({
},
})
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(CertificateFileSelection))
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(CertificateFileSelection) as any)
@@ -1,18 +1,18 @@
import * as React from 'react'
import CertificateFileSelection from './CertificateFileSelection'
import BrowserCertificateFileSelection from './BrowserCertificateFileSelection'
import Undo from '@material-ui/icons/Undo'
import Undo from '@mui/icons-material/Undo'
import { bindActionCreators } from 'redux'
import { Button, Grid } from '@material-ui/core'
import { Button, Grid } from '@mui/material'
import { connect } from 'react-redux'
import { connectionManagerActions } from '../../actions'
import { ConnectionOptions } from '../../model/ConnectionOptions'
import { Theme, withStyles } from '@material-ui/core/styles'
import { Theme } from '@mui/material/styles'
import { withStyles } from '@mui/styles'
import { isBrowserMode } from '../../utils/browserMode'
// Check if we're in browser mode
const isBrowserMode =
typeof window !== 'undefined' && (typeof process === 'undefined' || process.env?.BROWSER_MODE === 'true')
const CertSelector = isBrowserMode ? BrowserCertificateFileSelection : CertificateFileSelection
// Use browser or desktop file selection based on mode
const CertSelector: any = isBrowserMode ? BrowserCertificateFileSelection : CertificateFileSelection
interface Props {
connection: ConnectionOptions
@@ -110,4 +110,4 @@ const styles = (theme: Theme) => ({
},
})
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(Certificates))
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(Certificates) as any)
@@ -1,14 +1,14 @@
import ConnectionHealthIndicator from '../helper/ConnectionHealthIndicator'
import PowerSettingsNew from '@material-ui/icons/PowerSettingsNew'
import PowerSettingsNew from '@mui/icons-material/PowerSettingsNew'
import React from 'react'
import { Button } from '@material-ui/core'
import { Button } from '@mui/material'
function ConnectButton(props: { connecting: boolean; classes: any; toggle: () => void }) {
const { classes, toggle, connecting } = props
if (connecting) {
return (
<Button variant="contained" color="primary" className={classes.button} onClick={toggle}>
<Button variant="contained" color="primary" className={classes.button} onClick={toggle} data-testid="abort-button">
<ConnectionHealthIndicator />
&nbsp;&nbsp;Abort
</Button>
@@ -16,7 +16,7 @@ function ConnectButton(props: { connecting: boolean; classes: any; toggle: () =>
}
return (
<Button variant="contained" color="primary" className={classes.button} onClick={toggle}>
<Button variant="contained" color="primary" className={classes.button} onClick={toggle} data-testid="connect-button">
<PowerSettingsNew /> Connect
</Button>
)
@@ -1,17 +1,18 @@
import ConnectButton from './ConnectButton'
import React, { useCallback, useState } from 'react'
import Save from '@material-ui/icons/Save'
import Delete from '@material-ui/icons/Delete'
import Settings from '@material-ui/icons/Settings'
import Visibility from '@material-ui/icons/Visibility'
import VisibilityOff from '@material-ui/icons/VisibilityOff'
import Save from '@mui/icons-material/Save'
import Delete from '@mui/icons-material/Delete'
import Settings from '@mui/icons-material/Settings'
import Visibility from '@mui/icons-material/Visibility'
import VisibilityOff from '@mui/icons-material/VisibilityOff'
import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { connectionActions, connectionManagerActions } from '../../actions'
import { ConnectionOptions, toMqttConnection } from '../../model/ConnectionOptions'
import { KeyCodes } from '../../utils/KeyCodes'
import { Theme, withStyles } from '@material-ui/core/styles'
import { Theme } from '@mui/material/styles'
import { withStyles } from '@mui/styles'
import { ToggleSwitch } from './ToggleSwitch'
import { useGlobalKeyEventHandler } from '../../effects/useGlobalKeyEventHandler'
import {
@@ -24,7 +25,7 @@ import {
InputLabel,
MenuItem,
TextField,
} from '@material-ui/core'
} from '@mui/material'
interface Props {
connection: ConnectionOptions
@@ -235,6 +236,7 @@ function ConnectionSettings(props: Props) {
variant="contained"
className={classes.button}
onClick={props.managerActions.toggleAdvancedSettings}
data-testid="advanced-button"
>
<Settings /> Advanced
</Button>
@@ -285,4 +287,4 @@ const styles = (theme: Theme) => ({
},
})
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ConnectionSettings))
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ConnectionSettings) as any)
@@ -1,15 +1,19 @@
import * as React from 'react'
import ConnectionSettings from './ConnectionSettings'
const ConnectionSettingsAny = ConnectionSettings as any
import ProfileList from './ProfileList'
import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { connectionManagerActions } from '../../actions'
import { ConnectionOptions, toMqttConnection } from '../../model/ConnectionOptions'
import { Theme, withStyles } from '@material-ui/core/styles'
import { Modal, Paper, Toolbar, Typography, Collapse } from '@material-ui/core'
import { Theme } from '@mui/material/styles'
import { withStyles } from '@mui/styles'
import { Modal, Paper, Toolbar, Typography, Collapse } from '@mui/material'
import AdvancedConnectionSettings from './AdvancedConnectionSettings'
const AdvancedConnectionSettingsAny = AdvancedConnectionSettings as any
import Certificates from './Certificates'
const CertificatesAny = Certificates as any
interface Props {
actions: any
@@ -34,13 +38,13 @@ class ConnectionSetup extends React.PureComponent<Props, {}> {
return (
<div>
<Collapse in={!showAdvancedSettings && !showCertificateSettings}>
<ConnectionSettings connection={connection} />
<ConnectionSettingsAny connection={connection} />
</Collapse>
<Collapse in={showAdvancedSettings && !showCertificateSettings}>
<AdvancedConnectionSettings connection={connection} />
<AdvancedConnectionSettingsAny connection={connection} />
</Collapse>
<Collapse in={showCertificateSettings}>
<Certificates connection={connection} />
<CertificatesAny connection={connection} />
</Collapse>
</div>
)
@@ -111,7 +115,7 @@ const styles = (theme: Theme) => ({
textOverflow: 'ellipsis' as 'ellipsis',
whiteSpace: 'nowrap' as 'nowrap',
overflow: 'hidden' as 'hidden',
color: theme.palette.text.hint,
color: theme.palette.text.secondary,
fontSize: '0.9em',
marginLeft: theme.spacing(4),
},
@@ -134,4 +138,4 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ConnectionSetup))
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ConnectionSetup) as any)
@@ -1,7 +1,8 @@
import * as React from 'react'
import Add from '@material-ui/icons/Add'
import { Fab } from '@material-ui/core'
import { Theme, withStyles } from '@material-ui/core/styles'
import Add from '@mui/icons-material/Add'
import { Fab } from '@mui/material'
import { Theme } from '@mui/material/styles'
import { withStyles } from '@mui/styles'
const styles = (theme: Theme) => ({
addButton: {
@@ -1,8 +1,10 @@
import React, { useCallback } from 'react'
import { connect } from 'react-redux'
import { ListItem, Typography } from '@material-ui/core'
import { ListItem, Typography, Box } from '@mui/material'
import { DragIndicator } from '@mui/icons-material'
import { toMqttConnection, ConnectionOptions } from '../../../model/ConnectionOptions'
import { withStyles, Theme } from '@material-ui/core/styles'
import { withStyles } from '@mui/styles'
import { Theme } from '@mui/material/styles'
import { bindActionCreators } from 'redux'
import { connectionActions, connectionManagerActions } from '../../../actions'
@@ -14,6 +16,9 @@ export interface Props {
}
selected: boolean
classes: any
onDragStart: (connectionId: string) => void
onDragOver: (e: React.DragEvent) => void
onDrop: (connectionId: string) => void
}
const ConnectionItem = (props: Props) => {
@@ -24,20 +29,48 @@ const ConnectionItem = (props: Props) => {
}
}, [props.connection, props])
const handleDragStart = (e: React.DragEvent) => {
e.stopPropagation()
props.onDragStart(props.connection.id)
}
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
props.onDragOver(e)
}
const handleDrop = (e: React.DragEvent) => {
e.preventDefault()
e.stopPropagation()
props.onDrop(props.connection.id)
}
const connection = props.connection.host && toMqttConnection(props.connection)
return (
<ListItem
button={true}
selected={props.selected}
style={{ display: 'block' }}
className={props.classes.itemContainer}
onClick={() => props.actions.connectionManager.selectConnection(props.connection.id)}
onDoubleClick={() => {
props.actions.connectionManager.selectConnection(props.connection.id)
connect()
}}
onDragOver={handleDragOver}
onDrop={handleDrop}
>
<Typography className={props.classes.name}>{props.connection.name || 'mqtt broker'}</Typography>
<Typography className={props.classes.details}>{connection && connection.url}</Typography>
<Box
className={props.classes.dragHandle}
draggable
onDragStart={handleDragStart}
>
<DragIndicator fontSize="small" />
</Box>
<Box className={props.classes.textContainer}>
<Typography className={props.classes.name}>{props.connection.name || 'mqtt broker'}</Typography>
<Typography className={props.classes.details}>{connection && connection.url}</Typography>
</Box>
</ListItem>
)
}
@@ -62,9 +95,28 @@ export const connectionItemStyle = (theme: Theme) => ({
textOverflow: 'ellipsis' as 'ellipsis',
whiteSpace: 'nowrap' as 'nowrap',
overflow: 'hidden' as 'hidden',
color: theme.palette.text.hint,
color: theme.palette.text.secondary,
fontSize: '0.7em',
},
itemContainer: {
display: 'flex' as 'flex',
alignItems: 'center' as 'center',
padding: '8px 8px 8px 8px',
},
textContainer: {
flex: 1,
overflow: 'hidden' as 'hidden',
},
dragHandle: {
display: 'flex' as 'flex',
alignItems: 'center' as 'center',
marginRight: '8px',
cursor: 'grab' as 'grab',
color: theme.palette.text.secondary,
'&:active': {
cursor: 'grabbing' as 'grabbing',
},
},
})
export default connect(null, mapDispatchToProps)(withStyles(connectionItemStyle)(ConnectionItem))
export default connect(null, mapDispatchToProps)(withStyles(connectionItemStyle)(ConnectionItem) as any)
@@ -1,5 +1,6 @@
import ConnectionItem from './ConnectionItem'
import React from 'react'
const ConnectionItemAny = ConnectionItem as any
import React, { useState } from 'react'
import { AddButton } from './AddButton'
import { AppState } from '../../../reducers'
import { bindActionCreators } from 'redux'
@@ -7,8 +8,9 @@ import { connect } from 'react-redux'
import { connectionManagerActions } from '../../../actions'
import { ConnectionOptions } from '../../../model/ConnectionOptions'
import { KeyCodes } from '../../../utils/KeyCodes'
import { List } from '@material-ui/core'
import { Theme, withStyles } from '@material-ui/core/styles'
import { List } from '@mui/material'
import { Theme } from '@mui/material/styles'
import { withStyles } from '@mui/styles'
import { useGlobalKeyEventHandler } from '../../../effects/useGlobalKeyEventHandler'
interface Props {
@@ -20,13 +22,14 @@ interface Props {
function ProfileList(props: Props) {
const { actions, classes, connections, selected } = props
const [draggedConnectionId, setDraggedConnectionId] = useState<string | null>(null)
const selectConnection = (dir: 'next' | 'previous') => (event: KeyboardEvent) => {
if (!selected) {
return
}
const indexDirection = dir === 'next' ? 1 : -1
const connectionArray = Object.values(connections)
const connectionArray = Object.values(connections).sort((a, b) => (a.order || 0) - (b.order || 0))
const selectedIndex = connectionArray.map(connection => connection.id).indexOf(selected)
const nextConnection = connectionArray[selectedIndex + indexDirection]
if (nextConnection) {
@@ -38,6 +41,39 @@ function ProfileList(props: Props) {
useGlobalKeyEventHandler(KeyCodes.arrow_down, selectConnection('next'))
useGlobalKeyEventHandler(KeyCodes.arrow_up, selectConnection('previous'))
const handleDragStart = (connectionId: string) => {
setDraggedConnectionId(connectionId)
}
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault()
}
const handleDrop = (targetConnectionId: string) => {
if (!draggedConnectionId || draggedConnectionId === targetConnectionId) {
setDraggedConnectionId(null)
return
}
const sortedConnections = Object.values(connections).sort((a, b) => (a.order || 0) - (b.order || 0))
const draggedIndex = sortedConnections.findIndex(c => c.id === draggedConnectionId)
const targetIndex = sortedConnections.findIndex(c => c.id === targetConnectionId)
if (draggedIndex === -1 || targetIndex === -1) {
setDraggedConnectionId(null)
return
}
// Swap order values
const draggedConnection = sortedConnections[draggedIndex]
const targetConnection = sortedConnections[targetIndex]
actions.updateConnection(draggedConnection.id, { order: targetConnection.order })
actions.updateConnection(targetConnection.id, { order: draggedConnection.order })
setDraggedConnectionId(null)
}
const createConnectionButton = (
<div style={{ padding: '8px 16px' }}>
<AddButton action={actions.createConnection} />
@@ -48,9 +84,18 @@ function ProfileList(props: Props) {
return (
<List style={{ height: '100%' }} component="nav" subheader={createConnectionButton}>
<div className={classes.list}>
{Object.values(connections).map(connection => (
<ConnectionItem connection={connection} key={connection.id} selected={selected === connection.id} />
))}
{Object.values(connections)
.sort((a, b) => (a.order || 0) - (b.order || 0))
.map(connection => (
<ConnectionItemAny
connection={connection}
key={connection.id}
selected={selected === connection.id}
onDragStart={handleDragStart}
onDragOver={handleDragOver}
onDrop={handleDrop}
/>
))}
</div>
</List>
)
@@ -77,4 +122,4 @@ const mapStateToProps = (state: AppState) => {
}
}
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ProfileList))
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ProfileList) as any)
@@ -1,5 +1,5 @@
import React, { useCallback, useState } from 'react'
import Delete from '@material-ui/icons/Delete'
import Delete from '@mui/icons-material/Delete'
import { connectionManagerActions } from '../../actions'
import { ConnectionOptions } from '../../model/ConnectionOptions'
import {
@@ -12,9 +12,9 @@ import {
TableBody,
Paper,
Theme,
} from '@material-ui/core'
} from '@mui/material'
import { bindActionCreators } from 'redux'
import { withStyles } from '@material-ui/styles'
import { withStyles } from '@mui/styles'
import { connect } from 'react-redux'
function Subscriptions(props: {
@@ -87,4 +87,4 @@ const styles = (theme: Theme) => ({
},
})
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(Subscriptions))
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(Subscriptions) as any)
@@ -1,5 +1,5 @@
import React from 'react'
import { FormControlLabel, Switch } from '@material-ui/core'
import { FormControlLabel, Switch } from '@mui/material'
export function ToggleSwitch(props: { value: boolean; classes: any; toggle: () => void; label: string }) {
const { classes, value, toggle, label } = props
+2 -1
View File
@@ -1,5 +1,6 @@
import * as React from 'react'
import { Theme, withStyles } from '@material-ui/core'
import { Theme } from '@mui/material/styles'
import { withStyles } from '@mui/styles'
interface Props {
keyboardKey: string
+3 -2
View File
@@ -1,5 +1,6 @@
import * as React from 'react'
import { Theme, withStyles } from '@material-ui/core'
import { Theme } from '@mui/material/styles'
import { withStyles } from '@mui/styles'
const cursor = require('./cursor.png')
interface State {
@@ -74,7 +75,7 @@ const style = (theme: Theme) => ({
height: '32px',
position: 'fixed' as 'fixed',
zIndex: 1000000,
filter: theme.palette.type === 'light' ? undefined : 'invert(100%)',
filter: theme.palette.mode === 'light' ? undefined : 'invert(100%)',
pointerEvents: 'none' as 'none',
},
})
+2 -1
View File
@@ -1,5 +1,6 @@
import * as React from 'react'
import { Theme, withStyles } from '@material-ui/core'
import { Theme } from '@mui/material/styles'
import { withStyles } from '@mui/styles'
import Key from './Key'
interface State {
+8 -6
View File
@@ -1,9 +1,10 @@
import * as React from 'react'
import PersistentStorage from '../utils/PersistentStorage'
import SentimentDissatisfied from '@material-ui/icons/SentimentDissatisfied'
import Warning from '@material-ui/icons/Warning'
import { Theme, withStyles } from '@material-ui/core/styles'
import { Button, Modal, Paper, Toolbar, Typography } from '@material-ui/core'
import SentimentDissatisfied from '@mui/icons-material/SentimentDissatisfied'
import Warning from '@mui/icons-material/Warning'
import { Theme } from '@mui/material/styles'
import { withStyles } from '@mui/styles'
import { Button, Modal, Paper, Toolbar, Typography } from '@mui/material'
interface State {
error?: Error
@@ -11,6 +12,7 @@ interface State {
interface Props {
classes: any
children?: React.ReactNode
}
class ErrorBoundary extends React.PureComponent<Props, State> {
@@ -23,12 +25,12 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
}
private restart = () => {
window.location = window.location
window.location.reload()
}
private clearStorage = () => {
PersistentStorage.clear()
window.location = window.location
window.location.reload()
}
public componentDidCatch(error: Error, errorInfo: any) {
+23 -10
View File
@@ -1,13 +1,16 @@
import * as React from 'react'
import ChartPanel from '../ChartPanel'
import ReactSplitPane from 'react-split-pane'
import ReactSplitPaneImport from 'react-split-pane'
import Tree from '../Tree'
import { AppState } from '../../reducers'
import { ChartParameters } from '../../reducers/Charts'
import { connect } from 'react-redux'
import { List } from 'immutable'
import { Sidebar } from '../Sidebar'
import ReactResizeDetector from 'react-resize-detector'
import { useResizeDetector } from 'react-resize-detector'
// Type cast to any to work around React 18 compatibility issues with react-split-pane 0.1.x
const ReactSplitPane = ReactSplitPaneImport as any
interface Props {
heightProperty: any
@@ -21,11 +24,23 @@ function ContentView(props: Props) {
const [sidebarWidth, setSidebarWidth] = React.useState<string | number>('40%')
const [detectedHeight, setDetectedHeight] = React.useState(0)
const [detectedSidebarWidth, setDetectedSidebarWidth] = React.useState(0)
const detectSize = React.useCallback((width, newHeight) => {
const { height: resizeHeight, ref: heightRef } = useResizeDetector()
const { width: resizeWidth, ref: widthRef } = useResizeDetector()
React.useEffect(() => {
if (resizeHeight) setDetectedHeight(resizeHeight)
}, [resizeHeight])
React.useEffect(() => {
if (resizeWidth) setDetectedSidebarWidth(resizeWidth)
}, [resizeWidth])
const detectSize = React.useCallback((width: any, newHeight: any) => {
setDetectedHeight(newHeight)
}, [])
const detectSidebarSize = React.useCallback(width => {
const detectSidebarSize = React.useCallback((width: any) => {
setDetectedSidebarWidth(width)
}, [])
@@ -63,7 +78,7 @@ function ContentView(props: Props) {
split="vertical"
minSize={0}
size={sidebarWidth}
onChange={setSidebarWidth}
onChange={(size: number) => setSidebarWidth(size)}
onDragFinished={closeSidebarCompletelyIfItSitsOnTheEdge}
allowResize={true}
style={{ height: '100%' }}
@@ -80,20 +95,18 @@ function ContentView(props: Props) {
style={{ height: 'calc(100vh - 64px)' }}
pane1Style={{ maxHeight: '100%' }}
pane2Style={{ borderTop: '1px solid #999', display: 'flex' }}
onChange={setHeight}
onChange={(size: number) => setHeight(size)}
onDragFinished={closeDrawerCompletelyIfItSitsOnTheEdge}
>
<Tree />
{/** Passing height constraints via flex options down */}
<div style={{ flex: 1, display: 'flex', height: '100%', width: '100%' }}>
<div ref={heightRef} style={{ flex: 1, display: 'flex', height: '100%', width: '100%' }}>
{/** Resize detector must not be in the scroll zone, it needs to detect actual available size */}
<ReactResizeDetector handleHeight={true} onResize={detectSize} />
<ChartPanel />
</div>
</ReactSplitPane>
</span>
<div style={{ height: '100%' }}>
<ReactResizeDetector handleWidth={true} onResize={detectSidebarSize} />
<div ref={widthRef} style={{ height: '100%' }}>
<div
className={props.paneDefaults}
style={{ minWidth: '250px', height: '100%', overflowY: 'auto', overflowX: 'hidden' }}
+4 -3
View File
@@ -1,8 +1,9 @@
import * as React from 'react'
import { Snackbar, SnackbarContent } from '@material-ui/core'
import { Theme, withStyles } from '@material-ui/core/styles'
import { green, red } from '@material-ui/core/colors'
import { Snackbar, SnackbarContent } from '@mui/material'
import { Theme } from '@mui/material/styles'
import { withStyles } from '@mui/styles'
import { green, red } from '@mui/material/colors'
interface Props {
message?: string
+5 -4
View File
@@ -1,13 +1,14 @@
import * as React from 'react'
import * as q from '../../../../backend/src/Model'
import CustomIconButton from '../helper/CustomIconButton'
import Pause from '@material-ui/icons/PauseCircleFilled'
import Resume from '@material-ui/icons/PlayArrow'
import Pause from '@mui/icons-material/PauseCircleFilled'
import Resume from '@mui/icons-material/PlayArrow'
import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { treeActions } from '../../actions'
import { withStyles, Theme } from '@material-ui/core/styles'
import { withStyles } from '@mui/styles'
import { Theme } from '@mui/material/styles'
const styles = (theme: Theme) => ({
icon: {
@@ -102,4 +103,4 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(PauseButton))
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(PauseButton) as any)
+5 -4
View File
@@ -1,12 +1,13 @@
import React, { useCallback, useState, useRef } from 'react'
import ClearAdornment from '../helper/ClearAdornment'
import Search from '@material-ui/icons/Search'
import Search from '@mui/icons-material/Search'
import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { InputBase } from '@material-ui/core'
import { InputBase } from '@mui/material'
import { settingsActions } from '../../actions'
import { fade, Theme, withStyles } from '@material-ui/core/styles'
import { alpha as fade, Theme } from '@mui/material/styles'
import { withStyles } from '@mui/styles'
import { useGlobalKeyEventHandler } from '../../effects/useGlobalKeyEventHandler'
import { KeyCodes } from '../../utils/KeyCodes'
@@ -142,4 +143,4 @@ const styles = (theme: Theme) => ({
},
})
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(SearchBar))
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(SearchBar) as any)
+39 -6
View File
@@ -1,15 +1,19 @@
import * as React from 'react'
import CloudOff from '@material-ui/icons/CloudOff'
import CloudOff from '@mui/icons-material/CloudOff'
import Logout from '@mui/icons-material/Logout'
import ConnectionHealthIndicator from '../helper/ConnectionHealthIndicator'
import Menu from '@material-ui/icons/Menu'
const ConnectionHealthIndicatorAny = ConnectionHealthIndicator as any
import Menu from '@mui/icons-material/Menu'
import PauseButton from './PauseButton'
import SearchBar from './SearchBar'
import { AppBar, Button, IconButton, Toolbar, Typography } from '@material-ui/core'
import { AppBar, Button, IconButton, Toolbar, Typography } from '@mui/material'
import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { connectionActions, globalActions, settingsActions } from '../../actions'
import { Theme, withStyles } from '@material-ui/core/styles'
import { Theme } from '@mui/material/styles'
import { withStyles } from '@mui/styles'
import { isBrowserMode } from '../../utils/browserMode'
const styles = (theme: Theme) => ({
title: {
@@ -33,6 +37,9 @@ const styles = (theme: Theme) => ({
disconnect: {
margin: 'auto 8px auto auto',
},
logout: {
margin: 'auto 0 auto 8px',
},
disconnectLabel: {
color: theme.palette.primary.contrastText,
},
@@ -54,6 +61,22 @@ class TitleBar extends React.PureComponent<Props, {}> {
this.state = {}
}
private handleLogout = async () => {
// Disconnect first
this.props.actions.connection.disconnect()
// Clear credentials from sessionStorage
if (typeof sessionStorage !== 'undefined') {
sessionStorage.removeItem('mqtt-explorer-username')
sessionStorage.removeItem('mqtt-explorer-password')
}
// Reload page to reset all state and show login dialog
if (typeof window !== 'undefined') {
window.location.reload()
}
}
public render() {
const { actions, classes } = this.props
@@ -75,12 +98,22 @@ class TitleBar extends React.PureComponent<Props, {}> {
<PauseButton />
<Button
className={classes.disconnect}
classes={{ label: classes.disconnectLabel }}
sx={{ color: 'primary.contrastText' }}
onClick={actions.connection.disconnect}
data-testid="disconnect-button"
>
Disconnect <CloudOff className={classes.disconnectIcon} />
</Button>
<ConnectionHealthIndicator withBackground={true} />
{isBrowserMode && (
<Button
className={classes.logout}
sx={{ color: 'primary.contrastText' }}
onClick={this.handleLogout}
>
Logout <Logout className={classes.disconnectIcon} />
</Button>
)}
<ConnectionHealthIndicatorAny withBackground={true} />
</Toolbar>
</AppBar>
)
+87 -38
View File
@@ -1,57 +1,106 @@
import * as React from 'react'
import { Dialog, DialogTitle, DialogContent, DialogActions, TextField, Button, Typography } from '@material-ui/core'
import { Dialog, DialogTitle, DialogContent, DialogActions, TextField, Button, Typography } from '@mui/material'
interface LoginDialogProps {
open: boolean
onLogin: (username: string, password: string) => void
error?: string
waitTimeSeconds?: number
}
export function LoginDialog(props: LoginDialogProps) {
const [username, setUsername] = React.useState('')
const [password, setPassword] = React.useState('')
const [countdown, setCountdown] = React.useState<number | undefined>(props.waitTimeSeconds)
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
// Update countdown when waitTimeSeconds prop changes
React.useEffect(() => {
setCountdown(props.waitTimeSeconds)
}, [props.waitTimeSeconds])
// Countdown timer
React.useEffect(() => {
if (countdown === undefined || countdown <= 0) {
return
}
const timer = setInterval(() => {
setCountdown(prev => {
if (prev === undefined || prev <= 1) {
return undefined
}
return prev - 1
})
}, 1000)
return () => clearInterval(timer)
}, [countdown])
const handleLogin = () => {
if (countdown !== undefined && countdown > 0) {
// Don't allow login during countdown
return
}
if (!username || !password) {
// Don't allow empty credentials
return
}
props.onLogin(username, password)
}
const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
handleLogin()
}
}
const isDisabled = countdown !== undefined && countdown > 0
return (
<Dialog open={props.open} disableEscapeKeyDown disableBackdropClick>
<form onSubmit={handleSubmit}>
<DialogTitle>Login to MQTT Explorer</DialogTitle>
<DialogContent>
{props.error && (
<Typography color="error" style={{ marginBottom: 16 }}>
{props.error}
</Typography>
)}
<TextField
autoFocus
margin="dense"
label="Username"
type="text"
fullWidth
value={username}
onChange={e => setUsername(e.target.value)}
required
/>
<TextField
margin="dense"
label="Password"
type="password"
fullWidth
value={password}
onChange={e => setPassword(e.target.value)}
required
/>
</DialogContent>
<DialogActions>
<Button type="submit" color="primary" variant="contained">
Login
</Button>
</DialogActions>
</form>
<Dialog open={props.open} disableEscapeKeyDown onClose={(event, reason) => { if (reason !== 'backdropClick') { /* Allow closing only via escape if needed */ } }}>
<DialogTitle>Login to MQTT Explorer</DialogTitle>
<DialogContent>
{props.error && (
<Typography color="error" style={{ marginBottom: 16 }}>
{props.error}
</Typography>
)}
{countdown !== undefined && countdown > 0 && (
<Typography color="warning" style={{ marginBottom: 16, fontWeight: 'bold' }}>
Please wait {countdown} seconds before trying again...
</Typography>
)}
<TextField
autoFocus
margin="dense"
label="Username"
type="text"
fullWidth
value={username}
onChange={e => setUsername(e.target.value)}
onKeyPress={handleKeyPress}
disabled={isDisabled}
required
data-testid="username-input"
/>
<TextField
margin="dense"
label="Password"
type="password"
fullWidth
value={password}
onChange={e => setPassword(e.target.value)}
onKeyPress={handleKeyPress}
disabled={isDisabled}
required
data-testid="password-input"
/>
</DialogContent>
<DialogActions>
<Button onClick={handleLogin} color="primary" variant="contained" disabled={isDisabled}>
{isDisabled ? `Wait ${countdown}s` : 'Login'}
</Button>
</DialogActions>
</Dialog>
)
}
+1 -1
View File
@@ -1,5 +1,5 @@
import * as React from 'react'
import { TextField, MenuItem, Tooltip } from '@material-ui/core'
import { TextField, MenuItem, Tooltip } from '@mui/material'
import { QoS } from '../../../backend/src/DataSource/MqttSource'
export function QosSelect(props: { selected: QoS; onChange: (value: QoS) => void; label?: string }) {
@@ -1,9 +1,9 @@
import * as React from 'react'
import { InputLabel, Switch, Theme, Tooltip } from '@material-ui/core'
import { withStyles } from '@material-ui/styles'
import { InputLabel, Switch, Theme, Tooltip } from '@mui/material'
import { withStyles } from '@mui/styles'
const sha1 = require('sha1')
function BooleanSwitch(props: { title: string; value: boolean; tooltip: string; action: () => void; classes: any }) {
function BooleanSwitch(props: { title: string; value: boolean; tooltip: string; action: () => void; classes: any; 'data-testid'?: string }) {
const { tooltip, value, action, title, classes } = props
const clickHandler = (e: React.MouseEvent) => {
@@ -20,7 +20,13 @@ function BooleanSwitch(props: { title: string; value: boolean; tooltip: string;
</InputLabel>
</Tooltip>
<Tooltip title={tooltip}>
<Switch name={`toggle-${sha1(title)}`} checked={value} onChange={action} color="primary" />
<Switch
name={`toggle-${sha1(title)}`}
checked={value}
onChange={action}
color="primary"
data-testid={props['data-testid']}
/>
</Tooltip>
</div>
)
@@ -3,9 +3,10 @@ import React, { useMemo } from 'react'
import { AppState } from '../../reducers'
import { Base64Message } from '../../../../backend/src/Model/Base64Message'
import { connect } from 'react-redux'
import { Theme, withStyles } from '@material-ui/core/styles'
import { Theme } from '@mui/material/styles'
import { withStyles } from '@mui/styles'
import { TopicViewModel } from '../../model/TopicViewModel'
import { Typography } from '@material-ui/core'
import { Typography } from '@mui/material'
import { usePollingToFetchTreeNode } from '../helper/usePollingToFetchTreeNode'
import { useUpdateComponentWhenNodeUpdates } from '../helper/useUpdateComponentWhenNodeUpdates'
const abbreviate = require('number-abbreviate')
@@ -19,7 +20,7 @@ const styles = (theme: Theme) => ({
container: {
width: '100%',
height: '224px',
backgroundColor: theme.palette.type === 'dark' ? 'rebeccapurple' : '#ebebeb',
backgroundColor: theme.palette.mode === 'dark' ? 'rebeccapurple' : '#ebebeb',
marginBottom: 0,
padding: '8px',
},
@@ -1,14 +1,15 @@
import * as React from 'react'
import BooleanSwitch from './BooleanSwitch'
import BrokerStatistics from './BrokerStatistics'
import ChevronRight from '@material-ui/icons/ChevronRight'
import ChevronRight from '@mui/icons-material/ChevronRight'
import TimeLocale from './TimeLocale'
import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { globalActions, settingsActions } from '../../actions'
import { shell } from 'electron'
import { Theme, withStyles } from '@material-ui/core/styles'
import { Theme } from '@mui/material/styles'
import { withStyles } from '@mui/styles'
import { TopicOrder } from '../../reducers/Settings'
import {
@@ -19,9 +20,10 @@ import {
InputLabel,
MenuItem,
Select,
SelectChangeEvent,
Typography,
Tooltip,
} from '@material-ui/core'
} from '@mui/material'
export const autoExpandLimitSet = [
{
@@ -70,7 +72,7 @@ const styles = (theme: Theme) => ({
},
author: {
margin: 'auto 8px 8px auto',
color: theme.palette.text.hint,
color: theme.palette.text.secondary,
cursor: 'pointer' as 'pointer',
},
})
@@ -136,6 +138,7 @@ class Settings extends React.PureComponent<Props, {}> {
tooltip="Enable dark theme"
value={theme === 'dark'}
action={actions.settings.toggleTheme}
data-testid="dark-mode-toggle"
/>
)
}
@@ -167,7 +170,7 @@ class Settings extends React.PureComponent<Props, {}> {
)
}
private onChangeAutoExpand = (e: React.ChangeEvent<{ value: unknown }>) => {
private onChangeAutoExpand = (e: SelectChangeEvent<number>) => {
this.props.actions.settings.setAutoExpandLimit(parseInt(String(e.target.value), 10))
}
@@ -199,7 +202,7 @@ class Settings extends React.PureComponent<Props, {}> {
)
}
private onChangeSorting = (e: React.ChangeEvent<{ value: unknown }>) => {
private onChangeSorting = (e: SelectChangeEvent<TopicOrder>) => {
this.props.actions.settings.setTopicOrder(e.target.value as TopicOrder)
}
@@ -3,9 +3,9 @@ import DateFormatter from '../helper/DateFormatter'
import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { Input, InputLabel, MenuItem, Select, Theme } from '@material-ui/core'
import { Input, InputLabel, MenuItem, Select, Theme } from '@mui/material'
import { settingsActions } from '../../actions'
import { withStyles } from '@material-ui/styles'
import { withStyles } from '@mui/styles'
function importAll(r: any) {
r.keys().forEach(r)
@@ -38,7 +38,7 @@ function TimeLocaleSettings(props: Props) {
</MenuItem>
))
function updateLocale(e: React.ChangeEvent<{ value: unknown }>) {
function updateLocale(e: any) {
const locale = e.target.value ? String(e.target.value) : ''
actions.settings.setTimeLocale(locale)
}
@@ -1,11 +1,11 @@
import * as q from '../../../../../backend/src/Model'
import * as React from 'react'
import ShowChart from '@material-ui/icons/ShowChart'
import ShowChart from '@mui/icons-material/ShowChart'
import TopicPlot from '../../TopicPlot'
import { bindActionCreators } from 'redux'
import { chartActions } from '../../../actions'
import { connect } from 'react-redux'
import { Fade, Paper, Popper, Tooltip } from '@material-ui/core'
import { Fade, Paper, Popper, Tooltip } from '@mui/material'
import { JsonPropertyLocation } from '../../../../../backend/src/JsonAstParser'
interface Props {
@@ -1,6 +1,6 @@
import * as React from 'react'
import { Theme } from '@material-ui/core'
import { withStyles } from '@material-ui/styles'
import { Theme } from '@mui/material'
import { withStyles } from '@mui/styles'
interface Props {
changes: Array<Diff.Change>
@@ -1,13 +1,13 @@
import * as diff from 'diff'
import * as q from '../../../../../backend/src/Model'
import * as React from 'react'
import Add from '@material-ui/icons/Add'
import Add from '@mui/icons-material/Add'
import ChartPreview from './ChartPreview'
import Remove from '@material-ui/icons/Remove'
import Remove from '@mui/icons-material/Remove'
import { JsonPropertyLocation } from '../../../../../backend/src/JsonAstParser'
import { lineChangeStyle, trimNewlineRight } from './util'
import { Theme } from '@material-ui/core'
import { withStyles } from '@material-ui/styles'
import { Theme } from '@mui/material'
import { withStyles } from '@mui/styles'
interface Props {
changes: Array<diff.Change>
@@ -8,7 +8,8 @@ import { isPlottable, lineChangeStyle, trimNewlineRight } from './util'
import { JsonPropertyLocation, literalsMappedByLines } from '../../../../../backend/src/JsonAstParser'
import { selectTextWithCtrlA } from '../../../utils/handleTextSelectWithCtrlA'
import { style } from './style'
import { withStyles, Typography } from '@material-ui/core'
import { Typography } from '@mui/material'
import { withStyles } from '@mui/styles'
import 'prismjs/components/prism-json'
interface Props {
@@ -1,8 +1,8 @@
import { CodeBlockColors, CodeBlockColorsBraceMonokai } from '../CodeBlockColors'
import { Theme } from '@material-ui/core'
import { Theme } from '@mui/material'
export const style = (theme: Theme) => {
const codeBlockColors = theme.palette.type === 'light' ? CodeBlockColors : CodeBlockColorsBraceMonokai
const codeBlockColors = theme.palette.mode === 'light' ? CodeBlockColors : CodeBlockColorsBraceMonokai
const codeBaseStyle = {
font: "12px/normal 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', 'source-code-pro', monospace",
display: 'inline-grid' as 'inline-grid',
+4 -2
View File
@@ -1,7 +1,8 @@
import React, { useCallback, useState, useEffect, memo } from 'react'
import { Badge, Typography } from '@material-ui/core'
import { Badge, Typography } from '@mui/material'
import { selectTextWithCtrlA } from '../../utils/handleTextSelectWithCtrlA'
import { Theme, withStyles, emphasize } from '@material-ui/core/styles'
import { Theme, emphasize } from '@mui/material/styles'
import { withStyles } from '@mui/styles'
interface HistoryItem {
key: string
@@ -87,6 +88,7 @@ function HistoryDrawer(props: Props) {
invisible={!visible}
badgeContent={props.items.length}
color="primary"
data-testid="message-history"
>
{expanded ? '▼ History' : '▶ History'}
</Badge>
+1 -1
View File
@@ -1,6 +1,6 @@
import React, { memo } from 'react'
import { Message } from '../../../../backend/src/Model'
import { Tooltip } from '@material-ui/core'
import { Tooltip } from '@mui/material'
export const MessageId = memo(function MessageId(props: { message: Message; addComma?: boolean }) {
const { message, addComma } = props
+1 -1
View File
@@ -1,7 +1,7 @@
import * as q from '../../../../backend/src/Model'
import * as React from 'react'
import { TopicViewModel } from '../../model/TopicViewModel'
import { Typography } from '@material-ui/core'
import { Typography } from '@mui/material'
interface Props {
node?: q.TreeNode<TopicViewModel>
+8 -8
View File
@@ -1,7 +1,7 @@
import React from 'react'
import ExpandMore from '@material-ui/icons/ExpandMore'
import { ExpansionPanel, ExpansionPanelDetails, ExpansionPanelSummary, Typography, Theme } from '@material-ui/core'
import { withStyles } from '@material-ui/styles'
import ExpandMore from '@mui/icons-material/ExpandMore'
import { Accordion, AccordionDetails, AccordionSummary, Typography, Theme } from '@mui/material'
import { withStyles } from '@mui/styles'
const styles = (theme: Theme) => ({
summary: { minHeight: '0' },
@@ -19,14 +19,14 @@ const Panel = (props: {
detailsHidden?: boolean
}) => {
return (
<ExpansionPanel defaultExpanded={true} disabled={props.disabled}>
<ExpansionPanelSummary expandIcon={<ExpandMore />} className={props.classes.summary}>
<Accordion defaultExpanded={true} disabled={props.disabled}>
<AccordionSummary expandIcon={<ExpandMore />} className={props.classes.summary}>
<Typography className={props.classes.heading}>{props.children[0]}</Typography>
</ExpansionPanelSummary>
</AccordionSummary>
{props.detailsHidden ? null : (
<ExpansionPanelDetails className={props.classes.detail}>{props.children[1]}</ExpansionPanelDetails>
<AccordionDetails className={props.classes.detail}>{props.children[1]}</AccordionDetails>
)}
</ExpansionPanel>
</Accordion>
)
}
@@ -1,6 +1,6 @@
import * as React from 'react'
import { default as AceEditor } from 'react-ace'
import { Theme, withTheme } from '@material-ui/core'
import { useTheme } from '@mui/material/styles'
import 'ace-builds'
import 'ace-builds/webpack-resolver'
import 'ace-builds/src-noconflict/mode-json'
@@ -13,11 +13,11 @@ import 'react-ace'
function Editor(props: {
editorMode: string
theme: Theme
value: string | undefined
onChange: (value: string) => void
editorRef: React.Ref<AceEditor>
}) {
const theme = useTheme()
const editorOptions = {
showLineNumbers: false,
tabSize: 2,
@@ -28,7 +28,7 @@ function Editor(props: {
ref={props.editorRef}
style={{}}
mode={props.editorMode}
theme={props.theme.palette.type === 'dark' ? 'monokai' : 'dawn'}
theme={theme.palette.mode === 'dark' ? 'monokai' : 'dawn'}
name="UNIQUE_ID_OF_DIV"
width="100%"
height="200px"
@@ -45,4 +45,4 @@ function Editor(props: {
)
}
export default withTheme(Editor)
export default Editor
@@ -1,5 +1,5 @@
import * as React from 'react'
import { FormControlLabel, Radio, RadioGroup } from '@material-ui/core'
import { FormControlLabel, Radio, RadioGroup } from '@mui/material'
interface Props {
value: string
@@ -1,14 +1,14 @@
import Editor from './Editor'
import { AttachFileOutlined, FormatAlignLeft } from '@material-ui/icons'
import { AttachFileOutlined, FormatAlignLeft } from '@mui/icons-material'
import Message from './Model/Message'
import Navigation from '@material-ui/icons/Navigation'
import Navigation from '@mui/icons-material/Navigation'
import PublishHistory from './PublishHistory'
import React, { useCallback, useMemo, useState, useRef, memo } from 'react'
import RetainSwitch from './RetainSwitch'
import TopicInput from './TopicInput'
import { AppState } from '../../../reducers'
import { bindActionCreators } from 'redux'
import { Button, Fab, Theme, Tooltip, withTheme } from '@material-ui/core'
import { Button, Fab, Tooltip } from '@mui/material'
import { connect } from 'react-redux'
import { EditorModeSelect } from './EditorModeSelect'
import { globalActions, publishActions } from '../../../actions'
@@ -23,7 +23,6 @@ interface Props {
globalActions: typeof globalActions
retain: boolean
editorMode: string
theme: Theme
}
function useHistory(): [Array<Message>, (topic: string, payload?: string) => void] {
@@ -221,4 +220,4 @@ const mapStateToProps = (state: AppState) => {
}
}
export default connect(mapStateToProps, mapDispatchToProps)(withTheme(Publish))
export default connect(mapStateToProps, mapDispatchToProps)(Publish)
@@ -1,6 +1,6 @@
import QosSelect from './QosPublishOption'
import React from 'react'
import { Checkbox, FormControlLabel, Tooltip } from '@material-ui/core'
import { Checkbox, FormControlLabel, Tooltip } from '@mui/material'
import { publishActions } from '../../../actions'
import { bindActionCreators } from 'redux'
import { AppState } from '../../../reducers'
@@ -1,6 +1,6 @@
import ClearAdornment from '../../helper/ClearAdornment'
import React, { useCallback, useMemo, useRef } from 'react'
import { FormControl, Input, InputLabel } from '@material-ui/core'
import { FormControl, Input, InputLabel } from '@mui/material'
import { publishActions } from '../../../actions'
import { bindActionCreators } from 'redux'
import { AppState } from '../../../reducers'
+7 -5
View File
@@ -2,12 +2,14 @@ import * as q from '../../../../backend/src/Model'
import React, { useState, useEffect, useCallback } from 'react'
import NodeStats from './NodeStats'
import ValuePanel from './ValueRenderer/ValuePanel'
const ValuePanelAny = ValuePanel as any
import { AppState } from '../../reducers'
import { ExpansionPanelDetails } from '@material-ui/core'
import { AccordionDetails } from '@mui/material'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { settingsActions, sidebarActions } from '../../actions'
import { Theme, withStyles } from '@material-ui/core/styles'
import { Theme } from '@mui/material/styles'
import { withStyles } from '@mui/styles'
import { TopicViewModel } from '../../model/TopicViewModel'
import TopicPanel from './TopicPanel/TopicPanel'
import Panel from './Panel'
@@ -56,16 +58,16 @@ function Sidebar(props: Props) {
<div id="Sidebar" className={classes.drawer}>
<div>
<TopicPanel node={node} />
<ValuePanel lastUpdate={node ? node.lastUpdate : 0} />
<ValuePanelAny lastUpdate={node ? node.lastUpdate : 0} />
<Panel>
<span>Publish</span>
<Publish connectionId={props.connectionId} />
</Panel>
<Panel detailsHidden={!node}>
<span>Stats</span>
<ExpansionPanelDetails className={classes.details}>
<AccordionDetails className={classes.details}>
<NodeStats node={node} />
</ExpansionPanelDetails>
</AccordionDetails>
</Panel>
</div>
</div>
@@ -1,8 +1,8 @@
import * as q from '../../../../../backend/src/Model'
import CustomIconButton from '../../helper/CustomIconButton'
import Delete from '@material-ui/icons/Delete'
import Delete from '@mui/icons-material/Delete'
import React, { useCallback } from 'react'
import { Badge } from '@material-ui/core'
import { Badge } from '@mui/material'
export const RecursiveTopicDeleteButton = (props: {
node?: q.TreeNode<any>
@@ -1,7 +1,8 @@
import React from 'react'
import * as q from '../../../../../backend/src/Model'
import Button from '@material-ui/core/Button'
import { withStyles, Theme } from '@material-ui/core/styles'
import Button from '@mui/material/Button'
import { withStyles } from '@mui/styles'
import { Theme } from '@mui/material/styles'
import { treeActions } from '../../../actions'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
@@ -39,8 +40,8 @@ class Topic extends React.PureComponent<Props, {}> {
<Button
onClick={() => this.props.actions.selectTopic(edge!.target)}
size="small"
variant={theme.palette.type === 'light' ? 'contained' : undefined}
color={theme.palette.type === 'light' ? 'primary' : 'secondary'}
variant={theme.palette.mode === 'light' ? 'contained' : undefined}
color={theme.palette.mode === 'light' ? 'primary' : 'secondary'}
className={this.props.classes.button}
key={edge!.hash()}
>
@@ -66,4 +67,4 @@ const mapDispatchToProps = (dispatch: any) => {
}
}
export default connect(null, mapDispatchToProps)(withStyles(styles, { withTheme: true })(Topic))
export default connect(null, mapDispatchToProps)(withStyles(styles, { withTheme: true })(Topic) as any)
@@ -1,6 +1,6 @@
import * as q from '../../../../../backend/src/Model'
import CustomIconButton from '../../helper/CustomIconButton'
import Delete from '@material-ui/icons/Delete'
import Delete from '@mui/icons-material/Delete'
import React from 'react'
export const TopicDeleteButton = (props: {
@@ -3,6 +3,7 @@ import Copy from '../../helper/Copy'
import Panel from '../Panel'
import React, { useMemo, useCallback } from 'react'
import Topic from './Topic'
const TopicAny = Topic as any
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { RecursiveTopicDeleteButton } from './RecursiveTopicDeleteButton'
@@ -31,7 +32,7 @@ const TopicPanel = (props: { node?: q.TreeNode<any>; actions: typeof sidebarActi
<RecursiveTopicDeleteButton node={node} deleteTopicAction={deleteTopic} />
<TopicTypeButton node={node} />
</span>
<Topic node={node} />
<TopicAny node={node} />
</Panel>
),
[node, node?.childTopicCount()]
@@ -1,15 +1,15 @@
import React, { useCallback, useMemo } from 'react'
import * as q from '../../../../../backend/src/Model'
import ClickAwayListener from '@material-ui/core/ClickAwayListener'
import Grow from '@material-ui/core/Grow'
import Button from '@material-ui/core/Button'
import Paper from '@material-ui/core/Paper'
import Popper from '@material-ui/core/Popper'
import MenuItem from '@material-ui/core/MenuItem'
import MenuList from '@material-ui/core/MenuList'
import WarningRounded from '@material-ui/icons/WarningRounded'
import ClickAwayListener from '@mui/material/ClickAwayListener'
import Grow from '@mui/material/Grow'
import Button from '@mui/material/Button'
import Paper from '@mui/material/Paper'
import Popper from '@mui/material/Popper'
import MenuItem from '@mui/material/MenuItem'
import MenuList from '@mui/material/MenuList'
import WarningRounded from '@mui/icons-material/WarningRounded'
import { MessageDecoder, decoders } from '../../../decoders'
import { Tooltip } from '@material-ui/core'
import { Tooltip } from '@mui/material'
export const TopicTypeButton = (props: { node?: q.TreeNode<any> }) => {
const { node } = props
@@ -46,7 +46,7 @@ export const TopicTypeButton = (props: { node?: q.TreeNode<any> }) => {
[open]
)
const handleClose = useCallback((event: React.MouseEvent<Document, MouseEvent>) => {
const handleClose = useCallback((event: any) => {
if (anchorEl && anchorEl.contains(event.target as HTMLElement)) {
return
}
@@ -1,10 +1,12 @@
import React, { useCallback } from 'react'
import Code from '@material-ui/icons/Code'
import Reorder from '@material-ui/icons/Reorder'
import ToggleButton from '@material-ui/lab/ToggleButton'
import ToggleButtonGroup from '@material-ui/lab/ToggleButtonGroup'
import Code from '@mui/icons-material/Code'
import Reorder from '@mui/icons-material/Reorder'
import ToggleButton from '@mui/lab/ToggleButton'
import ToggleButtonGroup from '@mui/lab/ToggleButtonGroup'
import { settingsActions } from '../../../actions'
import { Tooltip, withStyles, Theme } from '@material-ui/core'
import { Tooltip } from '@mui/material'
import { withStyles } from '@mui/styles'
import { Theme } from '@mui/material/styles'
import { bindActionCreators } from 'redux'
import { AppState } from '../../../reducers'
import { connect } from 'react-redux'
@@ -73,4 +75,4 @@ const mapStateToProps = (state: AppState) => {
}
}
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ActionButtons))
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ActionButtons) as any)
@@ -1,7 +1,7 @@
import Clear from '@material-ui/icons/Clear'
import Clear from '@mui/icons-material/Clear'
import React, { useMemo } from 'react'
import { bindActionCreators } from 'redux'
import { Button, Tooltip } from '@material-ui/core'
import { Button, Tooltip } from '@mui/material'
import { connect } from 'react-redux'
import { sidebarActions } from '../../../actions'
@@ -1,6 +1,6 @@
import * as q from '../../../../../backend/src/Model'
import * as React from 'react'
import ShowChart from '@material-ui/icons/ShowChart'
import ShowChart from '@mui/icons-material/ShowChart'
import Copy from '../../helper/Copy'
import DateFormatter from '../../helper/DateFormatter'
import History from '../HistoryDrawer'
@@ -9,7 +9,9 @@ import React, { useCallback } from 'react'
import ValueRenderer from './ValueRenderer'
import { AppState } from '../../../reducers'
import { bindActionCreators } from 'redux'
import { Theme, Typography, withStyles } from '@material-ui/core'
import { Typography } from '@mui/material'
import { Theme } from '@mui/material/styles'
import { withStyles } from '@mui/styles'
import { connect } from 'react-redux'
import { sidebarActions } from '../../../actions'
import DeleteSelectedTopicButton from './DeleteSelectedTopicButton'
@@ -143,4 +145,4 @@ const styles = (theme: Theme) => ({
})
// @ts-ignore
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ValuePanel))
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ValuePanel) as any)
@@ -4,7 +4,7 @@ import CodeDiff from '../CodeDiff'
import { AppState } from '../../../reducers'
import { connect } from 'react-redux'
import { ValueRendererDisplayMode } from '../../../reducers/Settings'
import { Fade } from '@material-ui/core'
import { Fade } from '@mui/material'
import { Decoder } from '../../../../../backend/src/Model/Decoder'
import { useDecoder } from '../../hooks/useDecoder'
import { TopicViewModel } from '../../../model/TopicViewModel'
@@ -3,7 +3,8 @@ import React, { useEffect, useState, useMemo } from 'react'
import TreeNode from '.'
import { SettingsState } from '../../../reducers/Settings'
import { sortedNodes } from '../../../sortedNodes'
import { Theme, withStyles } from '@material-ui/core'
import { Theme } from '@mui/material/styles'
import { withStyles } from '@mui/styles'
import { TopicViewModel } from '../../../model/TopicViewModel'
import { treeActions } from '../../../actions'
@@ -1,6 +1,7 @@
import * as q from '../../../../../backend/src/Model'
import React, { memo } from 'react'
import { Theme, withStyles } from '@material-ui/core'
import { Theme } from '@mui/material/styles'
import { withStyles } from '@mui/styles'
import { TopicViewModel } from '../../../model/TopicViewModel'
import { useDecoder } from '../../hooks/useDecoder'
@@ -94,7 +95,7 @@ const styles = (theme: Theme) => ({
overflow: 'hidden' as 'hidden',
},
expander: {
color: theme.palette.type === 'light' ? '#222' : '#eee',
color: theme.palette.mode === 'light' ? '#222' : '#eee',
cursor: 'pointer' as 'pointer',
paddingRight: theme.spacing(0.25),
userSelect: 'none' as 'none',
+3 -2
View File
@@ -4,7 +4,8 @@ import TreeNodeSubnodes from './TreeNodeSubnodes'
import TreeNodeTitle from './TreeNodeTitle'
import { SettingsState } from '../../../reducers/Settings'
import { styles } from './styles'
import { Theme, withStyles } from '@material-ui/core/styles'
import { Theme } from '@mui/material/styles'
import { withStyles } from '@mui/styles'
import { TopicViewModel } from '../../../model/TopicViewModel'
import { treeActions } from '../../../actions'
import { useAnimationToIndicateTopicUpdate } from './effects/useAnimationToIndicateTopicUpdate'
@@ -35,7 +36,7 @@ function TreeNodeComponent(props: Props) {
const deleteTopicCallback = useDeleteKeyCallback(treeNode, actions)
useViewModelSubscriptions(treeNode, nodeRef, setSelected, setCollapsedOverride)
const animationClass =
props.theme.palette.type === 'light' ? props.classes.animationLight : props.classes.animationDark
props.theme.palette.mode === 'light' ? props.classes.animationLight : props.classes.animationDark
useAnimationToIndicateTopicUpdate(
nodeRef,
+4 -4
View File
@@ -1,5 +1,5 @@
import { blueGrey } from '@material-ui/core/colors'
import { Theme } from '@material-ui/core/styles'
import { blueGrey } from '@mui/material/colors'
import { Theme } from '@mui/material/styles'
export const styles = (theme: Theme) => {
return {
@@ -37,7 +37,7 @@ export const styles = (theme: Theme) => {
marginLeft: theme.spacing(1.5),
},
selected: {
backgroundColor: (theme.palette.type === 'light' ? blueGrey[300] : theme.palette.primary.main) + ' !important',
backgroundColor: (theme.palette.mode === 'light' ? blueGrey[300] : theme.palette.primary.main) + ' !important',
},
hover: {},
title: {
@@ -49,7 +49,7 @@ export const styles = (theme: Theme) => {
padding: '1px 4px 0 4px',
margin: '1px 0px',
'&:hover': {
backgroundColor: theme.palette.type === 'light' ? blueGrey[100] : theme.palette.primary.light,
backgroundColor: theme.palette.mode === 'light' ? blueGrey[100] : theme.palette.primary.light,
},
},
}
+8 -7
View File
@@ -1,17 +1,18 @@
import compareVersions from 'compare-versions'
import { compareVersions } from 'compare-versions'
import electron from 'electron'
import React from 'react'
import axios from 'axios'
import Close from '@material-ui/icons/Close'
import CloudDownload from '@material-ui/icons/CloudDownload'
import Close from '@mui/icons-material/Close'
import CloudDownload from '@mui/icons-material/CloudDownload'
import { AppState } from '../reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { green } from '@material-ui/core/colors'
import { Theme, withStyles } from '@material-ui/core/styles'
import { green } from '@mui/material/colors'
import { Theme } from '@mui/material/styles'
import { withStyles } from '@mui/styles'
import { updateNotifierActions } from '../actions'
import { Button, IconButton, Modal, Paper, Snackbar, SnackbarContent, Typography } from '@material-ui/core'
import { Button, IconButton, Modal, Paper, Snackbar, SnackbarContent, Typography } from '@mui/material'
import { rendererRpc, getAppVersion } from '../../../events'
interface Props {
@@ -83,7 +84,7 @@ class UpdateNotifier extends React.PureComponent<Props, State> {
return res.data as Array<GithubRelease>
}
private onCloseNotification = (event: React.SyntheticEvent<any>, reason: string) => {
private onCloseNotification = (event: any, reason: any) => {
if (reason === 'clickaway') {
return
}
+7 -6
View File
@@ -1,25 +1,26 @@
import * as React from 'react'
import Clear from '@material-ui/icons/Clear'
import { IconButton, Theme } from '@material-ui/core'
import { withTheme } from '@material-ui/core/styles'
import Clear from '@mui/icons-material/Clear'
import { IconButton, Theme } from '@mui/material'
import { useTheme } from '@mui/material/styles'
interface Props {
value?: string
action: any
style?: React.CSSProperties
variant?: 'primary'
theme: Theme
}
/**
* Clear button for text input fields
*/
function ClearAdornment(props: Props) {
const theme = useTheme()
if (!props.value) {
return null
}
const color = props.variant === 'primary' ? props.theme.palette.primary.contrastText : undefined
const color = props.variant === 'primary' ? theme.palette.primary.contrastText : undefined
return (
<IconButton style={{ ...props.style, padding: '1px' }} onClick={props.action}>
<Clear style={{ color, fontSize: '16px' }} />
@@ -27,4 +28,4 @@ function ClearAdornment(props: Props) {
)
}
export default withTheme(ClearAdornment)
export default ClearAdornment
@@ -1,11 +1,12 @@
import * as React from 'react'
import DeviceHubOutlined from '@material-ui/icons/DeviceHubOutlined'
import DeviceHubOutlined from '@mui/icons-material/DeviceHubOutlined'
import { AppState } from '../../reducers'
import { connect } from 'react-redux'
import { ConnectionHealth } from '../../reducers/Connection'
import { green, orange, red } from '@material-ui/core/colors'
import { withStyles, Theme } from '@material-ui/core/styles'
import { Tooltip } from '@material-ui/core'
import { green, orange, red } from '@mui/material/colors'
import { Theme } from '@mui/material/styles'
import { withStyles } from '@mui/styles'
import { Tooltip } from '@mui/material'
const styles = (theme: Theme) => ({
offline: {
@@ -65,4 +66,4 @@ const mapStateToProps = (state: AppState) => {
}
}
export default connect(mapStateToProps)(withStyles(styles)(ConnectionHealthIndicator))
export default connect(mapStateToProps)(withStyles(styles)(ConnectionHealthIndicator) as any)
+36 -11
View File
@@ -1,12 +1,28 @@
import * as React from 'react'
import Check from '@material-ui/icons/Check'
import Check from '@mui/icons-material/Check'
import CustomIconButton from './CustomIconButton'
import FileCopy from '@material-ui/icons/FileCopy'
import FileCopy from '@mui/icons-material/FileCopy'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { globalActions } from '../../actions'
const copy = require('copy-text-to-clipboard')
// Fallback for older browsers or when clipboard API is not available
const copyTextFallback = require('copy-text-to-clipboard')
async function copyToClipboard(text: string): Promise<boolean> {
try {
// Try modern Clipboard API first (works in browser with HTTPS)
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text)
return true
}
} catch (error) {
console.warn('Clipboard API failed, using fallback:', error)
}
// Fallback to copy-text-to-clipboard library
return copyTextFallback(text)
}
interface Props {
value?: string
@@ -26,15 +42,24 @@ class Copy extends React.PureComponent<Props, State> {
this.state = { didCopy: false }
}
private handleClick = (event: React.MouseEvent) => {
private handleClick = async (event: React.MouseEvent) => {
event.stopPropagation()
copy(this.props.value ?? this.props.getValue?.())
this.props.actions.global.showNotification('Copied to clipboard')
this.setState({ didCopy: true })
setTimeout(() => {
this.setState({ didCopy: false })
}, 1500)
const text = this.props.value ?? this.props.getValue?.()
if (!text) {
return
}
const success = await copyToClipboard(text)
if (success) {
this.props.actions.global.showNotification('Copied to clipboard')
this.setState({ didCopy: true })
setTimeout(() => {
this.setState({ didCopy: false })
}, 1500)
} else {
this.props.actions.global.showNotification('Failed to copy to clipboard')
}
}
public render() {
@@ -45,7 +70,7 @@ class Copy extends React.PureComponent<Props, State> {
)
return (
<CustomIconButton onClick={this.handleClick} tooltip="Copy to clipboard">
<CustomIconButton onClick={this.handleClick} tooltip="Copy to clipboard" data-testid="copy-button">
<div style={{ marginTop: '2px' }}>{icon}</div>
</CustomIconButton>
)

Some files were not shown because too many files have changed in this diff Show More