Compare commits

..
Author SHA1 Message Date
copilot-swe-agent[bot]andthomasnordquist 497ec6f61f Add auto-approve workflow for GitHub Copilot PRs
Co-authored-by: thomasnordquist <7721625+thomasnordquist@users.noreply.github.com>
2025-12-22 22:44:54 +00:00
copilot-swe-agent[bot] be8c588ac8 Initial plan 2025-12-22 22:41:03 +00:00
Copilotandthomasnordquist 0c1d09a8a0 Fix shell cd commands in npm scripts using subshells (#962)
The Docker browser build was failing because npm scripts used `cd app &&
command && cd ..`, which is unreliable in Docker RUN contexts and
certain shell environments.

## Changes

Updated all scripts using directory changes to use subshell isolation:

```diff
-"build:server": "npx tsc && cd app && npx webpack --config webpack.browser.config.mjs --mode production && cd ..",
+"build:server": "npx tsc && (cd app && npx webpack --config webpack.browser.config.mjs --mode production)",
```

**Updated scripts:**
- `build:server` - Docker browser build (primary fix)
- `build` - Electron build
- `install` - Dependency installation
- `test:app`, `test:backend` - Test runners (used in CI)
- `dev:app`, `dev:server:app` - Development servers

## Technical details

Subshell approach `(cd dir && command)`:
- Isolates directory change to subshell scope
- Auto-returns to parent directory on subshell exit
- More reliable across sh/bash/docker environments
- Eliminates manual `cd ..` restoration

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



<!-- START COPILOT ORIGINAL PROMPT -->



<details>

<summary>Original prompt</summary>

> The build is still failing, can't cd into app.


</details>



<!-- 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>
2025-12-22 22:23:14 +01:00
Copilotandthomasnordquist 308b748d0e Remove git dependency from Docker build via yarn resolution (#961)
Docker browser build fails because `@electron/rebuild` requires
`@electron/node-gyp` from a GitHub URL, which needs git to clone. The
package exists on npm registry but yarn.lock referenced the git URL.

## Changes

- **Added yarn resolution** to force
`@electron/node-gyp@10.2.0-electron.1` from npm registry instead of
GitHub
- **Removed git installation** from Dockerfile.browser builder stage
- **Updated yarn.lock** to resolve dependency from
`registry.yarnpkg.com` instead of `github.com`

```diff
// package.json
+ "resolutions": {
+   "@electron/node-gyp": "10.2.0-electron.1"
+ }
```

```diff
// Dockerfile.browser
- RUN apk add --no-cache git
```

This eliminates the git dependency entirely while preserving the exact
same package version.

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



<!-- START COPILOT ORIGINAL PROMPT -->



<details>

<summary>Original prompt</summary>

> build still fails
https://github.com/thomasnordquist/MQTT-Explorer/actions/runs/20443384267/job/58741359390


</details>



<!-- 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>
2025-12-22 22:02:18 +01:00
Copilotandthomasnordquist 85475a9201 Fix Docker build failure: add git to builder stage (#960)
The Docker browser build fails because yarn cannot install
`@electron/node-gyp` from GitHub without git.

## Changes

- Add git to builder stage in `Dockerfile.browser`
- Git is excluded from final production image via multi-stage build

## Technical Details

The `yarn.lock` contains a git-based dependency:
```
"@electron/node-gyp@https://github.com/electron/node-gyp#06b29aafb7708acef8b3669835c8a7857ebc92d2"
```

Alpine Linux base image lacks git by default. Installing it only in the
builder stage (where `yarn install` runs) resolves the build failure
without affecting production image size.

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



<!-- START COPILOT ORIGINAL PROMPT -->



<details>

<summary>Original prompt</summary>

> fix
https://github.com/thomasnordquist/MQTT-Explorer/actions/runs/20442696351/job/58739200179


</details>



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

💡 You can make Copilot smarter by setting up custom instructions,
customizing its development environment and configuring Model Context
Protocol (MCP) servers. Learn more [Copilot coding agent
tips](https://gh.io/copilot-coding-agent-tips) in the docs.

---------

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-22 21:38:04 +01:00
2c147a92ad Add Docker build for browser mode with optimized 3-stage build, multi-platform support, comprehensive UI testing, one-click deployment, enterprise SSO integration, and biweekly CI pipeline (#934)
## Docker Build for Browser Solution

This PR creates a Docker build for the browser solution (MQTT Explorer
server mode).

### Completed:
- [x] Create a production Dockerfile for the browser solution
(`Dockerfile.browser`)
  - **NEW**: 3-stage build for maximum optimization
- **NEW**: Clean production dependency installation with `yarn
--production`
  - **NEW**: Only compiled dist/ folder copied (no source code)
  - Alpine Linux base with Node.js 24
  - Non-root user (UID 1001) for security
  - Health check endpoint with proper error handling
  - Proper signal handling with dumb-init
  - Production dependencies automatically filtered by yarn
- [x] Apply Docker best practices (multi-stage build, minimal image,
non-root user, .dockerignore)
  - Created comprehensive .dockerignore
  - **FIXED**: Removed events from .dockerignore (needed for build)
  - **NEW**: Optimized for smaller layers with combined RUN commands
  - **NEW**: Removed development dependencies from final image
  - Used alpine base image
- [x] Create GitHub Actions workflow for building, publishing, and
testing the Docker image
  - Builds for linux/amd64, linux/arm64, linux/arm/v7
- **FIXED**: Added tsconfig.json and events/** to workflow trigger paths
- **FIXED**: Attestation now uses correct digest from build step output
  - **NEW**: Mosquitto MQTT broker service for integration testing
- **NEW**: MQTT broker configurable via MQTT_BROKER_HOST and
MQTT_BROKER_PORT environment variables
  - **NEW**: Full UI test suite runs against containerized application
- Tests container startup, health check, HTTP response, data persistence
  - **NEW**: Image size reporting in workflow summary
  - Tests verify application works with MQTT broker
- Publishes to GitHub Container Registry
(ghcr.io/thomasnordquist/mqtt-explorer)
  - Includes build attestation for supply chain security
- [x] Configure workflow to run on push and every two weeks via cron
schedule
  - Runs on 1st and 15th of each month at 2:00 AM UTC
- Also runs on push to master/beta/release branches when relevant files
change
  - Manual trigger via workflow_dispatch
- [x] Add comprehensive test suite
- Basic smoke tests: startup, health check, HTTP response, data
persistence
- **NEW**: Full UI test suite (`test:browser`) runs against Docker
container
- **NEW**: Tests connect to configurable MQTT broker (default
localhost:1883)
- Tests execute with Mosquitto MQTT broker available for backend
integration
  - Same comprehensive tests validate both Electron and browser modes
- [x] Test organization and naming
- **NEW**: Renamed `test:ui` to `test:electron` for Electron-specific
tests
- **NEW**: Added `test:browser` script for browser mode tests (runs same
UI test suite)
  - **NEW**: Kept `test:ui` as backward-compatible alias
- **NEW**: Renamed `ui-tests` workflow job to `electron-tests` for
clarity
- [x] Update documentation with Docker usage instructions
  - Created DOCKER.md with comprehensive Docker documentation
  - Updated README.md with Docker quick start
  - **UPDATED**: CI_CD.md now lists all 10 test steps accurately
  - **NEW**: Added one-click deployment options section
  - **NEW**: Added authentication modes documentation
- [x] **NEW**: One-click deployment solutions
  - **NEW**: Created docker-compose.yml for easy deployment
- **NEW**: Added Play with Docker (PWD) badge for instant browser-based
demo
  - **NEW**: Added DigitalOcean App Platform deployment badge
  - **NEW**: Added Koyeb deployment badge
  - **NEW**: Comprehensive deployment options documentation in DOCKER.md
- **NEW**: "Try It Now" section in README.md and DOCKER.md with PWD
badge
- [x] **NEW**: Enterprise authentication integration
  - **NEW**: Added `MQTT_EXPLORER_SKIP_AUTH` environment variable
- **NEW**: Allows disabling built-in authentication for proxy-based auth
(OAuth2 Proxy, Authelia, enterprise SSO)
- **NEW**: Socket.IO emits `auth-status` event on connection with
authentication state
- **NEW**: Frontend receives auth status via Socket.IO and skips login
dialog when disabled
  - **NEW**: Logout button hidden when authentication is disabled
- **NEW**: Created AuthContext for managing authentication state across
components
- **NEW**: Comprehensive security warnings in documentation about using
skip auth only behind trusted authentication proxies
- **NEW**: Updated docker-compose.yml with commented example for proxy
authentication
- [x] Code review and security scan passed
  - Fixed health check to handle connection errors properly
  - Corrected cron schedule comment
  - No security vulnerabilities found
  - Fixed image tag naming consistency
  - Simplified dependency management
- **FIXED**: Workflow trigger paths now include all build-affecting
files
  - **FIXED**: Attestation digest reference corrected
  - **FIXED**: events directory included in Docker build context
- **FIXED**: Mosquitto service properly configured for integration
testing
- **FIXED**: MQTT broker connection now configurable for flexible
testing environments
- **IMPROVED**: Auth status now communicated via Socket.IO for better
real-time synchronization
- [x] Rename image to ghcr.io/thomasnordquist/mqtt-explorer (removed
-browser suffix)
- [x] Add multi-platform support for Raspberry Pi
  - linux/arm64 (Raspberry Pi 3/4/5)
  - linux/arm/v7 (Raspberry Pi 2/3)
- [x] Upgrade to Node.js 24 (matching project requirements)
- [x] **NEW**: Optimize Docker image for minimal size
  - Only production dependencies (no devDependencies)
  - No backend source code (only compiled JavaScript)
  - Removed build tools and dev dependencies
  - Combined layers for smaller image
  - **Image size reported in workflow summary**
- [x] **NEW**: Fix webpack build configuration
  - **Enable minification** for production builds (was disabled)
  - **Update Material-UI references** from @material-ui to @mui
  - Fix vendor chunking to include @mui and @emotion packages
  - Reduces bundle size and fixes missing component issues

### Docker Image Features:
- **Base**: Alpine Linux with Node.js 24
- **Size**: Reported automatically in workflow summary
- **Platforms**: amd64, arm64, arm/v7 (Raspberry Pi support)
- **Security**: Non-root user, minimal attack surface
- **Reliability**: Health checks, graceful shutdown
- **Persistence**: Data volume at `/app/data`
- **Registry**: ghcr.io/thomasnordquist/mqtt-explorer
- **Runtime deps**: Only production dependencies (automatically
filtered)
- **Frontend**: Minified webpack bundles with proper vendor splitting
- **Testing**: Full UI test suite with configurable MQTT broker
integration
- **One-Click Deploy**: Play with Docker, DigitalOcean, Koyeb
- **Enterprise Ready**: Optional authentication bypass for proxy-based
SSO

### Available Tags:
- `latest` - Latest stable from master
- `master`, `beta`, `release` - Latest from each branch
- `<branch>-<sha>` - Specific commits

### Authentication Options:
1. **Standard Mode** (default): Built-in username/password
authentication
- Set credentials via `MQTT_EXPLORER_USERNAME` and
`MQTT_EXPLORER_PASSWORD` environment variables
2. **Skip Authentication Mode**: Set `MQTT_EXPLORER_SKIP_AUTH=true` for
proxy-based auth
- Use only behind trusted authentication proxies (OAuth2 Proxy,
Authelia, enterprise SSO)
- Socket.IO automatically informs frontend about auth status on
connection
- Frontend skips login dialog and logout button is hidden when
authentication is disabled
- ⚠️ **Security Warning**: Only use in environments with external
authentication protection

### One-Click Deployment:
Try MQTT Explorer instantly without installation:
- **Play with Docker**: Free browser-based demo (click badge in
README.md or DOCKER.md)
- **DigitalOcean**: Deploy to managed platform starting at $5/month
- **Koyeb**: Deploy to global edge network with free tier

### Security Summary:
- CodeQL scan passed with no vulnerabilities
- Docker image runs as non-root user (UID 1001)
- Multi-stage build reduces attack surface
- Health check includes proper error handling
- Minimal runtime dependencies reduce vulnerability exposure
- Full UI test suite validates application functionality
- Build attestation with correct digest reference
- MQTT broker integration tested with configurable connection via
environment variables
- Optional authentication bypass for enterprise SSO integration (with
comprehensive security warnings)
- Auth status communicated via Socket.IO for real-time synchronization

<!-- START COPILOT ORIGINAL PROMPT -->



<details>

<summary>Original prompt</summary>

> 
> ----
> 
> *This section details on the original issue you should resolve*
> 
> <issue_title>create a docker build for the browser
solution</issue_title>
> <issue_description>Create a docker build for amd64, that ships with a
minimal image including nodes. Apply best practices and create a test
workflow that builds it , publishes it and tests the built image. Build
it every two weeks</issue_description>
> 
> ## Comments on the Issue (you are @copilot in this section)
> 
> <comments>
> </comments>
> 


</details>



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

- Fixes thomasnordquist/MQTT-Explorer#933

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

💡 You can make Copilot smarter by setting up custom instructions,
customizing its development environment and configuring Model Context
Protocol (MCP) servers. Learn more [Copilot coding agent
tips](https://gh.io/copilot-coding-agent-tips) in the docs.

---------

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>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-12-22 21:06:35 +01:00
Copilot a143c5fb45 Upgrade to Material-UI v7 and React 19 (#954) 2025-12-22 21:03:46 +01: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
Copilot 92aa2c9fa8 Fix UI test timeouts, TypeScript compilation, dependency compatibility, and backend tests with isolated test suite using per-test mocking (#930) 2025-12-20 15:09:26 +01:00
Copilot 5a54ba4983 Upgrade Electron to 39.2.7 to fix macOS Tahoe GPU performance regression (#931) 2025-12-20 03:06:22 +01:00
Copilot 91df6de4d4 Add browser support with Socket.io transport, authentication, performance-optimized IPC, and CI/CD (#925) 2025-12-20 02:35:34 +01:00
Copilot 8285627c5f Implement comprehensive UI test suite with meaningful assertions and best practices (#921) 2025-12-20 02:13:31 +01:00
Thomas Nordquist 55f8b7d2b7 Allow commercial use
Updated the license from Creative Commons Attribution-NonCommercial 4.0 to Attribution-ShareAlike 4.0. Adjusted terms and conditions to reflect the new license's requirements.
2025-12-19 23:30:08 +01:00
Copilot 8f1eeedbaf Configure comprehensive Copilot instructions for repository best practices (#923) 2025-12-19 22:01:29 +01:00
Copilot 4843b2ec18 Add MCP introspection support for Electron frontend with Copilot agent integration (#916) 2025-12-19 21:46:43 +01:00
Thomas Nordquist 803413a087 Merge pull request #897 from scubanarc/license
Update license to CC by-nd
2025-08-22 21:19:01 +02:00
Jason Bauer b457559b4a Update license 2025-08-21 12:54:21 -07:00
Björn Dalfors 03ba43038c Merge pull request #812 from thomasnordquist/chore/fix-broken-test-locators
fix broken test locators
2024-06-17 09:31:25 +00:00
Björn Dalfors 8975e7b641 Merge pull request #813 from thomasnordquist/chore/dont-use-pull-request-target-for-untrusted-code
dont use pull_request_target as it opens the repo for pwnage..
2024-06-17 09:30:23 +00:00
Björn Dalfors efc9fb9736 dont use pull_request_target as it opens the repo for pwnage.. 2024-06-17 11:25:07 +02:00
Björn Dalfors 61f2389c1c fix broken test locators 2024-06-17 10:01:34 +02:00
Thomas Nordquist f539e03c7e Merge pull request #801 from thomasnordquist/feat/set-payload-from-file
feat: support save and load payload from file
2024-06-02 08:35:53 +02:00
Thomas Nordquist 724ea5acbf Merge pull request #804 from thomasnordquist/chore/add-sparkplug-to-demovideo
add sparkplug decoding to demo video
2024-06-02 08:34:48 +02:00
Björn Dalfors e009940530 fix inputs not being cleared 2024-06-01 22:24:41 +02:00
Björn Dalfors e19178780f add sparkplug decoding to demo video 2024-06-01 22:24:41 +02:00
Björn Dalfors 3229ef5643 Chore/fix workflow sha (#807)
* checkout merge commit of PR, not base branch head

https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target

* use link to test result, gif exceeds github allowed content length
2024-06-01 22:23:25 +02:00
Björn Dalfors b4a6199936 Move file operation to backend 2024-05-29 10:02:00 +02:00
Björn Dalfors bd6a1a0d2d Support specifying file encoding 2024-05-29 10:01:32 +02:00
Björn Dalfors 9d09ab2165 move filesystem operation to backend 2024-05-27 22:09:12 +02:00
Björn Dalfors f17640c9db feat: save value to file 2024-05-27 22:09:12 +02:00
Björn Dalfors 1ba0d07757 feat: support set payload from file when publishing 2024-05-27 22:09:12 +02:00
Thomas Nordquist 20a3202b5f Merge pull request #802 from thomasnordquist/tnordquist/fix-hot-reload
chore: fix webpack reload
2024-05-27 18:06:08 +02:00
Thomas Nordquist 28b99f5774 chore: fix webpack reload 2024-05-27 18:05:24 +02:00
Thomas Nordquist 42565c8bdc chore: coerce ui-test to end 2024-05-27 10:07:23 +02:00
Thomas Nordquist 8b43e20f2e Merge pull request #795 from thomasnordquist/tnordquist/decode-data-in-frontend
decode data in frontend
2024-05-25 16:28:53 +02:00
Thomas Nordquist a2a75588c9 Merge pull request #799 from thomasnordquist/tnordquist/allow-to-connect-with-double-click
feat: connect with double-click
2024-05-25 16:28:37 +02:00
Thomas Nordquist c13b60cd18 Merge pull request #800 from thomasnordquist/tnordquist/fix-eclipse-server
chore: update eclipse server url
2024-05-25 16:27:55 +02:00
Thomas Nordquist 18f8da9054 test: fix demo video 2024-05-24 22:29:49 +02:00
Thomas Nordquist f6856d66cc chore: update eclipse server url 2024-05-24 22:27:24 +02:00
Thomas Nordquist 79fbd34cfa feat: connect with double-click 2024-05-24 22:23:26 +02:00
Thomas Nordquist 3bc23e6d74 test: fix demo video 2024-05-24 22:01:13 +02:00
Thomas Nordquist e9a56ac48d Merge remote-tracking branch 'origin/master' into tnordquist/decode-data-in-frontend 2024-05-24 17:51:24 +02:00
Björn Dalfors b4bdd01808 add sparkplug messages to demovideo 2024-05-24 17:50:08 +02:00
Björn Dalfors 4406bf5de4 feat: use tahu for sparkplug decoding 2024-05-24 17:50:07 +02:00
Thomas Nordquist ae0ce79e26 Merge pull request #794 from thomasnordquist/feat/use-tahu-for-sparkplug-decoding
Feat/use tahu for sparkplug decoding
2024-05-24 10:12:21 +02:00
Thomas Nordquist bbe2ae3f29 test: fix tests 2024-05-23 23:19:37 +02:00
Thomas Nordquist a2c4388c78 fix: repair types 2024-05-23 17:05:27 +02:00
Thomas Nordquist c88978f0dd fix: fix ui updates 2024-05-22 15:12:45 +02:00
Thomas Nordquist b3a37e4794 chore: refactor 2024-05-22 14:44:06 +02:00
Thomas Nordquist 1ecb53b397 fix: update react when decoder has been overriden 2024-05-22 09:04:06 +02:00
Björn Dalfors 97fedcba08 fix sparkplug topic regexp 2024-05-21 15:26:43 +02:00
Björn Dalfors 1f23c65484 Stop click event propagation prevent panel from collapsing 2024-05-21 15:17:18 +02:00
Thomas Nordquist 980072f680 chore: decode data in frontend 2024-05-21 09:22:11 +02:00
Björn Dalfors c452b9f417 add sparkplug messages to demovideo 2024-05-20 16:42:54 +02:00
Björn Dalfors b04f5dee16 feat: use tahu for sparkplug decoding 2024-05-18 21:48:14 +02:00
Björn Dalfors 7617430a3f fix regex 2024-05-18 21:42:25 +02:00
Thomas Nordquist 10aae59c92 Merge remote-tracking branch 'fb/multi-decoder/master' into feat/use-tahu-for-sparkplug-decoding 2024-05-18 11:26:39 +02:00
Björn Dalfors f4bda3e242 feat: use tahu for sparkplug decoding 2024-05-17 16:14:32 +02:00
Björn Dalfors a346c48d3e refine sparkplug detection 2024-05-17 09:08:34 +02:00
Björn Dalfors 8a2c39ba8e fix: use sparkplugb decoder only for spBv1.0 topic 2024-05-15 15:24:30 +02:00
Thomas Nordquist 65b86ac5f6 chore: remove precondition 2024-05-11 21:55:11 +02:00
Thomas Nordquist 4ead740982 chore: allow manual update of website 2024-05-11 21:54:20 +02:00
Thomas Nordquist ddaf06b682 chore: remove precondition 2024-05-11 21:51:38 +02:00
Thomas Nordquist 9e18c4db4e chore: prevent new builds being uploaded without a release 2024-05-11 21:49:27 +02:00
Thomas Nordquist ee783f15c0 chore: move website update to separate release action 2024-05-11 21:44:28 +02:00
Thomas Nordquist 0c31843ed5 chore: reduce builds 2024-05-11 21:29:53 +02:00
Thomas Nordquist ffc93d20d8 chore: retry release beta 5 2024-05-11 21:17:47 +02:00
Thomas Nordquist 3c62da7e19 chore: only build macos dmg 2024-05-11 21:12:27 +02:00
Thomas Nordquist 6846dbbb28 chore: update website with new version 2024-05-11 21:10:57 +02:00
semantic-release-bot 270e3e3ead chore(release): 0.4.0-beta.5 [skip ci] 2024-05-11 15:47:26 +00:00
Thomas Nordquist 0d73b0f519 feat: test to trigger beta release 5 2024-05-11 17:46:23 +02:00
semantic-release-bot ee83022d19 chore(release): 0.4.0-beta.4 [skip ci] 2024-05-11 15:35:58 +00:00
Thomas Nordquist f27467ed97 fix: add macos 2024-05-11 17:34:48 +02:00
semantic-release-bot ddbf3d9f1f chore(release): 0.4.0-beta.3 [skip ci] 2024-05-11 14:19:25 +00:00
Thomas Nordquist 60bbbc54d7 chore: use GithubApp to bypass branch protection 2024-05-11 16:07:16 +02:00
Thomas Nordquist 538aa7dc29 chore: use custom token fo releases 2024-05-11 15:31:08 +02:00
Thomas Nordquist b37af291cc feat: skip package release until github and semantic-release are in sync 2024-05-11 14:42:17 +02:00
Thomas Nordquist 333a1bcabe fix: fix repository 2024-05-11 14:39:34 +02:00
Thomas Nordquist c2404a7eb6 Merge pull request #779 from thomasnordquist/dependabot/npm_and_yarn/ejs-3.1.10
Bump ejs from 3.1.9 to 3.1.10
2024-05-11 12:18:00 +02:00
dependabot[bot] bb602cd28e Bump ejs from 3.1.9 to 3.1.10
Bumps [ejs](https://github.com/mde/ejs) from 3.1.9 to 3.1.10.
- [Release notes](https://github.com/mde/ejs/releases)
- [Commits](https://github.com/mde/ejs/compare/v3.1.9...v3.1.10)

---
updated-dependencies:
- dependency-name: ejs
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-05-11 10:11:20 +00:00
Thomas Nordquist 898fd3896c Merge pull request #786 from thomasnordquist/tnordquist/test-ci
chore: add ci / cd piplelines as github actions
2024-05-11 12:10:36 +02:00
Thomas Nordquist 7fb0483889 chore: document release 2024-05-11 12:03:01 +02:00
Thomas Nordquist 7d77110c1f chore: upload test video as test artifact 2024-05-11 11:00:31 +02:00
Thomas Nordquist a7ce6b4419 chore: upload test video as test artifact 2024-05-10 22:30:02 +02:00
Thomas Nordquist 204f6dbcde chore: fix docker context 2024-05-10 22:17:39 +02:00
Thomas Nordquist 7fa997087f chore: build docker test image 2024-05-10 21:15:04 +02:00
semantic-release-bot a96b08eaaa chore(release): 0.4.0-beta.2 [skip ci] 2024-05-10 18:12:27 +00:00
Thomas Nordquist c9c997d13a fix: trigger build 2024-05-10 20:04:31 +02:00
semantic-release-bot 6f3e0f62e1 chore(release): 0.4.0-beta.1 [skip ci] 2024-05-10 18:03:44 +00:00
Thomas Nordquist 824b39637c chore: use newer node version 2024-05-10 19:56:01 +02:00
Thomas Nordquist 0100b2988a chore: dry-run 2024-05-10 18:52:25 +02:00
Thomas Nordquist 0e72329c77 fix: update semantic-release 2024-05-10 18:48:38 +02:00
Thomas Nordquist 9b7c3b8e9c feat: use semantic-release 2024-05-10 18:46:06 +02:00
Thomas Nordquist b9a5a5f1d8 add semantic release 2024-05-10 18:05:41 +02:00
Thomas Nordquist cc9cc411f0 skip osx builds for now 2024-05-10 17:50:07 +02:00
Thomas Nordquist 5f75079f2f add workflows 2024-05-10 17:44:23 +02:00
Björn Dalfors a6e16dcd17 Merge pull request #772 from thomasnordquist/chore/fix-spellchecker
upgrade cspell and fix spelling issues
2024-05-02 12:58:52 +02:00
Björn Dalfors f1b13a2919 upgrade cspell and fix spelling issues 2024-04-10 09:14:12 +02:00
Björn Dalfors 737afb3c1b Merge pull request #773 from thomasnordquist/chore/replace-spectron-with-playwright
Chore/replace spectron with playwright
2024-04-10 09:07:32 +02:00
Björn Dalfors e21011fce1 use --frozen-lockfile to ensure dependency integrity across builds 2024-04-08 10:46:22 +02:00
Björn Dalfors 613d0d7178 fix test script and video capture 2024-04-08 10:14:44 +02:00
Björn Dalfors bb964aba20 replace file-loader with assets to get demo mouse working again
fix mouse pointer
2024-04-06 21:19:18 +02:00
Björn Dalfors 6e5e2e0dd7 replace deprecated spectron with playwright 2024-04-05 14:35:18 +02:00
Björn Dalfors 5afccac2ac bump version 2024-04-03 17:32:57 +02:00
Björn Dalfors 4626df0bf7 Merge pull request #771 from thomasnordquist/chore/upgrade-mocha
upgrade mocha, remove deprecated mochaopts
2024-04-03 08:07:52 +01:00
Björn Dalfors 252780a51a upgrade mocha, remove deprecated mochaopts 2024-04-03 00:03:52 +02:00
Björn Dalfors 245e661159 remove accedentally added dependency 2024-04-03 00:02:38 +02:00
Björn Dalfors 659033e4eb Merge pull request #769 from thomasnordquist/dependabot/npm_and_yarn/app/webpack-dev-middleware-5.3.4
Bump webpack-dev-middleware from 5.3.3 to 5.3.4 in /app
2024-04-02 21:06:19 +01:00
dependabot[bot] 82fbf91f8a Bump webpack-dev-middleware from 5.3.3 to 5.3.4 in /app
Bumps [webpack-dev-middleware](https://github.com/webpack/webpack-dev-middleware) from 5.3.3 to 5.3.4.
- [Release notes](https://github.com/webpack/webpack-dev-middleware/releases)
- [Changelog](https://github.com/webpack/webpack-dev-middleware/blob/v5.3.4/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-middleware/compare/v5.3.3...v5.3.4)

---
updated-dependencies:
- dependency-name: webpack-dev-middleware
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-02 19:24:03 +00:00
Björn Dalfors 4275060003 upgrade protobufjs due cve 2024-04-02 21:21:46 +02:00
Björn Dalfors 6fe74d49fd Merge pull request #770 from thomasnordquist/dependabot/npm_and_yarn/app/follow-redirects-1.15.6
Bump follow-redirects from 1.15.5 to 1.15.6 in /app
2024-04-02 20:06:39 +01:00
Björn Dalfors 736145d9b4 Merge pull request #421 from jcwillox/patch-1
Simplify config path and increase portability
2024-04-02 19:33:26 +01:00
Björn Dalfors d5ad716086 Merge pull request #767 from thomasnordquist/dependabot/npm_and_yarn/app/express-4.19.2
Bump express from 4.18.3 to 4.19.2 in /app
2024-04-02 19:28:22 +01:00
Björn Dalfors c3f1e7c5e8 Merge pull request #763 from thomasnordquist/dependabot/npm_and_yarn/ua-parser-js-1.0.37
Bump ua-parser-js from 1.0.2 to 1.0.37
2024-04-02 19:27:31 +01:00
Björn Dalfors e1b6c709b0 Merge pull request #708 from thomasnordquist/dependabot/npm_and_yarn/minimist-1.2.8
Bump minimist from 1.2.5 to 1.2.8
2024-04-02 19:26:49 +01:00
dependabot[bot] 42de570a98 Bump follow-redirects from 1.15.5 to 1.15.6 in /app
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.5 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.5...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-02 18:24:38 +00:00
Björn Dalfors 30469e63e5 Merge pull request #768 from thomasnordquist/chore/upgrade-dependencies-2024
upgrade dependencies
2024-04-02 19:23:07 +01:00
Björn Dalfors 899db30e3b upgrade axios 2024-04-02 19:35:01 +02:00
Björn Dalfors 51d0eaafcc yarn upgrade 2024-04-02 17:03:29 +02:00
dependabot[bot] cfd1333989 Bump express from 4.18.3 to 4.19.2 in /app
Bumps [express](https://github.com/expressjs/express) from 4.18.3 to 4.19.2.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.18.3...4.19.2)

---
updated-dependencies:
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-02 14:43:11 +00:00
dependabot[bot] 6c9d98a763 Bump ua-parser-js from 1.0.2 to 1.0.37
Bumps [ua-parser-js](https://github.com/faisalman/ua-parser-js) from 1.0.2 to 1.0.37.
- [Release notes](https://github.com/faisalman/ua-parser-js/releases)
- [Changelog](https://github.com/faisalman/ua-parser-js/blob/1.0.37/changelog.md)
- [Commits](https://github.com/faisalman/ua-parser-js/compare/1.0.2...1.0.37)

---
updated-dependencies:
- dependency-name: ua-parser-js
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-02 14:39:11 +00:00
dependabot[bot] 040549c40a Bump minimist from 1.2.5 to 1.2.8
Bumps [minimist](https://github.com/minimistjs/minimist) from 1.2.5 to 1.2.8.
- [Release notes](https://github.com/minimistjs/minimist/releases)
- [Changelog](https://github.com/minimistjs/minimist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/minimistjs/minimist/compare/v1.2.5...v1.2.8)

---
updated-dependencies:
- dependency-name: minimist
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-02 14:38:09 +00:00
Björn Dalfors 0c6d777999 Merge pull request #757 from thomasnordquist/chore/upgrade-electron
Chore/upgrade electron
2024-04-02 15:34:34 +01:00
Björn Dalfors 65c53fd670 use node 18 to support github electron-builder action 2024-04-02 16:17:40 +02:00
Björn Dalfors 1b7c9c52f6 move sparkplug protocol to js file as file is not included when packaging
maybe there is a better way to resolve this..
2024-04-02 13:16:34 +02:00
Björn Dalfors a63b12b266 upgrade ts-node 2024-03-10 14:10:22 +01:00
Björn Dalfors a8ff4adde7 pin electron version 2024-03-10 13:06:05 +01:00
Björn Dalfors 3b8418ccfd supress type errors for now 2024-03-10 12:57:00 +01:00
Björn Dalfors b51b3065b0 add path to react types 2024-03-10 12:56:35 +01:00
Björn Dalfors 1f0c6771e5 upgrade dependencies to fix hashing algorithm throwing errors 2024-03-10 12:55:07 +01:00
Björn Dalfors 67277b4652 node 19 2024-03-10 12:52:42 +01:00
Björn Dalfors b0e30a896f upgrade electrong dependencies 2024-03-10 12:04:06 +01:00
Björn Dalfors 93010dc06e Merge pull request #756 from thomasnordquist/chore/fix-linting
Chore/fix linting
2024-03-10 11:48:29 +01:00
Björn Dalfors 302b8f3c21 disable spellcheck until later version that supports --no-exit-code is installed 2024-03-08 08:42:45 +01:00
Björn Dalfors 62d087bb0e fix tslint errors 2024-03-08 08:42:45 +01:00
Björn Dalfors f25cab4682 chore: upgrade prettier and fix linting errors 2024-03-08 08:42:41 +01:00
mhorsche 626b9cab7d Specific int/uint byte size
- possible data types are: 'json', 'string', 'hex', 'uint8', 'uint16', 'uint32', 'uint64', 'int8', 'int16', 'int32', 'int64', 'float', 'double'
- default is 'json'
2022-06-21 21:14:59 +02:00
Thomas Nordquist d1de0770f2 Merge pull request #592 from klaernie/patch-1
go for cloning the gh-pages branch directly
2022-02-27 23:03:15 +01:00
Thomas Nordquist 2a3f481a24 Merge pull request #629 from thomasnordquist/dubyte/add_sparkplug
Add sparkplug b support
2022-02-27 23:02:26 +01:00
Thomas Nordquist ed492ccbf4 refactor 2022-02-27 23:01:08 +01:00
Thomas Nordquist ea7535b250 apply suggestions from code review 2022-02-27 22:58:30 +01:00
Thomas Nordquist b17b54490e move sparkplug decoding to backend 2022-02-27 22:49:54 +01:00
Thomas Nordquist 72400af679 Merge branch 'master' into HEAD 2022-02-27 20:47:59 +01:00
Thomas Nordquist f8f1ddfebb fix moment locales bug 2022-02-27 20:22:45 +01:00
Thomas Nordquist 6e4d08e4b5 update mqtt library 2022-02-27 20:06:11 +01:00
Thomas Nordquist 5da8fe0f90 chore: remove after-sign 2022-02-27 19:09:25 +01:00
Thomas Nordquist df9eda4866 use node 16 for appveyor 2022-02-27 18:54:33 +01:00
Thomas Nordquist 6b030ab5ee remove package-lock 2022-02-27 18:50:23 +01:00
Thomas Nordquist e1493db7c8 add rpc system to improve ipc 2022-02-27 18:46:56 +01:00
Thomas Nordquist 205ea00c41 add package lock 2022-02-27 14:22:44 +01:00
Thomas Nordquist d428428a6e enforce npm version 2022-02-27 14:22:22 +01:00
Thomas Nordquist 24e9c4cd22 fix build 2022-02-27 14:15:50 +01:00
Thomas Nordquist d253c6c764 remove tracking 2022-02-27 14:15:01 +01:00
Thomas Nordquist 13b8f8d5da fix certificate selection 2022-02-27 13:33:22 +01:00
Andre Klärner e769ddece4 go for cloning the gh-pages branch directly
This avoids pulling in the entire history of the master branch
2021-10-03 22:36:04 +02:00
Sinuhe Tellez 64e807beef fix linter 2021-08-14 23:26:36 -04:00
Sinuhe Tellez 531af31490 remobe generated ts and load proto file directly 2021-08-14 23:24:17 -04:00
Sinuhe Tellez e708e1d0c7 update dependencies 2021-08-12 02:14:24 -04:00
Sinuhe Tellez 8fcf8b2478 format topicPlot 2021-08-11 19:26:10 -04:00
Sinuhe Tellez RiveraandThomas Nordquist 4fdd5b2063 Update topic plot about sparkplugb render
Co-authored-by: Thomas Nordquist <thomasnordquist@users.noreply.github.com>
2021-08-11 10:23:21 -04:00
Pawel Defée e4add31793 Accept smaller key files
I can generate a valid key file for my Mosquitto setup that fails the minimum size check of 128, for example this kind of key:

-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIFXWXY9yVJRKhZRfLz/oaAcjmOzX/2El+QRU+/1Npyfe
-----END PRIVATE KEY-----
2021-08-11 14:10:33 +02:00
Sinuhe Tellez 0cab2169c2 fix yarn build 2021-08-10 23:23:28 -04:00
Sinuhe Tellez 86f5b94852 fix linter issues 2021-08-10 22:59:59 -04:00
Sinuhe Tellez f60449c253 add protobuf dependency 2021-08-10 22:12:21 -04:00
Sinuhe Tellez c1c8b9aa61 update backend package.json and topicPlot 2021-08-10 22:06:07 -04:00
Sinuhe Tellez dcf692d193 remove un used dependencies 2021-08-10 21:51:25 -04:00
Sinuhe Tellez 7f757b9f8a remove unused import 2021-08-10 15:02:55 -04:00
Sinuhe Tellez 23b46cd432 decode diff view for sparkplug 2021-08-08 16:32:22 -04:00
Sinuhe Tellez 68ef9ac913 value render try sparkplug 2021-08-08 15:46:48 -04:00
Max Horsche 567f6d2d50 Added support for binary data types
- Select data type (string, json, hex, uint, int, float) for each topic individually
- Default data type is 'string'
- Show milliseconds in message received timestamp
2021-01-11 10:11:34 +01:00
Josh Willox 3fa47f0318 Simplify config path 2020-08-10 19:35:46 +10:00
Thomas Nordquist 9cdfa2de7b Prepare app for notarization 2020-04-28 23:58:26 +02:00
Thomas Nordquist 355e9177fc Update electron 2020-04-27 18:45:52 +02:00
Thomas Nordquist dbb6ead7ba Fix builds and snap upload 2020-04-25 14:04:46 +02:00
Thomas Nordquist ad888f6f9a Update version 2020-04-24 15:37:56 +02:00
Thomas Nordquist 07458cd712 Expose message Ids to the user 2020-04-20 18:27:51 +02:00
Thomas Nordquist 6fc0d3f28d Fix publish button 2020-04-20 14:03:03 +02:00
Thomas Nordquist 7a91e4dee6 Fix setting environmental for mocha tests in windows builds 2020-04-20 13:21:16 +02:00
Thomas Nordquist 3c40be97b7 Fix demoVideo 2020-04-20 13:09:37 +02:00
Thomas Nordquist ff00f7a99e Add app tests to yarn test 2020-04-20 12:31:57 +02:00
Thomas Nordquist b72fc48bdb Add quality of service option to subscriptions
Fixes #323, #14, #334
Fixes #132
2020-04-20 12:24:23 +02:00
Thomas Nordquist e87a7115c5 Revert "Only produce artifacts on appveyor when a release was tagged"
This reverts commit 487eee93cd.
2020-04-19 13:13:16 +02:00
Thomas Nordquist 14dac22732 Fix password input
Fixes #292
2020-04-19 12:51:29 +02:00
Thomas Nordquist ccdaaa6ce2 Fix codestyle 2020-04-16 12:19:25 +02:00
Thomas Nordquist 487eee93cd Only produce artifacts on appveyor when a release was tagged 2020-04-16 12:17:45 +02:00
Thomas Nordquist 59737edfb4 Always focus editor after pressing buttons in Publish sidebar component 2020-04-16 11:51:18 +02:00
Thomas Nordquist 30af13f793 Update linter 2020-04-16 11:00:34 +02:00
Thomas Nordquist 19e8bfdb37 Fix chart where properties contain periods
Fixes #281
2020-04-16 10:53:53 +02:00
Thomas Nordquist a89d7cf62e Improve json editor 2020-04-16 10:50:43 +02:00
Thomas Nordquist 65e9f2e074 Update dependecies 2020-04-16 10:43:41 +02:00
Thomas Nordquist ff66d10aaa Fix property name 2020-04-16 10:30:20 +02:00
Thomas Nordquist 5203d40a24 Restore original focus after hitting publish button 2020-04-16 00:40:22 +02:00
Thomas Nordquist 237c718a0a Update prettier 2020-04-15 23:54:30 +02:00
Thomas Nordquist fbfbe94d19 Prevent focussing filter when meta or ctrl key is pressed
Fixes copy/paste problem
2020-04-15 22:49:12 +02:00
Thomas Nordquist e24e505cc0 Use selected topic when clearing publish topic, also focus input element 2020-04-15 22:23:18 +02:00
Thomas Nordquist 28743ba646 Update dependecies 2020-04-15 21:26:27 +02:00
Thomas Nordquist f63e73ee0a Add documentation for how to update http://mqtt-explorer.com 2020-01-17 12:52:34 +01:00
Thomas Nordquist fbad7afa79 Update travis ubuntu dist 2020-01-17 11:50:55 +01:00
Thomas Nordquist 34cc38ab3c Fix build 2020-01-17 09:37:47 +01:00
Thomas Nordquist 8645789550 Fix object mutability 2019-11-13 17:27:02 +01:00
Thomas Nordquist 4db6e7b1d7 Update electron to 7.1.1 2019-11-13 17:26:27 +01:00
Thomas Nordquist 1fae61b1fa Update react-transision-groups 2019-11-13 15:35:27 +01:00
Thomas Nordquist b26eac3edb Update react-transition-group 2019-11-13 15:33:16 +01:00
Thomas Nordquist 6d1354bf07 Update React & Reace-ace 2019-11-13 15:31:24 +01:00
Thomas Nordquist 7148e302f6 Add mocha as dev dependency 2019-11-13 10:47:18 +01:00
Thomas Nordquist 763a1aea69 Update dependecies 2019-11-13 10:46:22 +01:00
Thomas Nordquist 94a92e9b29 Reset empty username/password to undefined
Fixes #229
2019-11-13 09:11:02 +01:00
Thomas Nordquist 922f90c4eb Fix memoization of chart y-range parameters 2019-11-13 09:06:20 +01:00
Thomas Nordquist ff11973a9a Extract chart range definition 2019-11-12 14:38:01 +01:00
Thomas Nordquist 29fc2eea4d Update app dependencies 2019-11-12 11:16:31 +01:00
Thomas Nordquist 577e1e6b10 Fix implicit peer dependency 2019-11-12 07:30:48 +01:00
Thomas Nordquist d113fab279 Fix plot range selection
Fixes #176
2019-11-12 07:26:05 +01:00
Thomas Nordquist a6c401ab6a Fix publish sidebar width
Fixes #227
2019-11-11 17:30:18 +01:00
Thomas Nordquist 6cc8c8810b Merge branch 'master' of github.com:thomasnordquist/MQTT-Explorer 2019-10-12 09:37:54 +02:00
dependabot-preview[bot] 040a29da30 Bump @types/react from 16.8.23 to 16.9.4 in /app (#215)
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 16.8.23 to 16.9.4.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-12 09:15:19 +02:00
dependabot-preview[bot] 45f31ad931 Bump cspell from 4.0.23 to 4.0.28 (#199)
Bumps [cspell](https://github.com/streetsidesoftware/cspell) from 4.0.23 to 4.0.28.
- [Release notes](https://github.com/streetsidesoftware/cspell/releases)
- [Commits](https://github.com/streetsidesoftware/cspell/compare/cspell@4.0.23...cspell@4.0.28)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-12 09:14:34 +02:00
dependabot-preview[bot] 39ddb984ab Bump @types/mocha from 5.2.6 to 5.2.7 (#201)
Bumps [@types/mocha](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/mocha) from 5.2.6 to 5.2.7.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/mocha)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-12 09:14:19 +02:00
dependabot-preview[bot] 197788eb79 Bump file-loader from 4.0.0 to 4.2.0 in /app (#213)
Bumps [file-loader](https://github.com/webpack-contrib/file-loader) from 4.0.0 to 4.2.0.
- [Release notes](https://github.com/webpack-contrib/file-loader/releases)
- [Changelog](https://github.com/webpack-contrib/file-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/file-loader/compare/v4.0.0...v4.2.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-12 09:13:57 +02:00
dependabot-preview[bot] c85bc0323c Bump typescript from 3.5.2 to 3.6.3 in /app (#216)
Bumps [typescript](https://github.com/Microsoft/TypeScript) from 3.5.2 to 3.6.3.
- [Release notes](https://github.com/Microsoft/TypeScript/releases)
- [Commits](https://github.com/Microsoft/TypeScript/compare/v3.5.2...v3.6.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-12 09:13:04 +02:00
dependabot-preview[bot] 65c2a6eff1 Bump @types/node from 12.6.8 to 12.7.8 in /app (#218)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 12.6.8 to 12.7.8.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-10-12 09:12:47 +02:00
dependabot-preview[bot] 5a4f3a0388 Bump mime from 2.4.3 to 2.4.4 (#164)
Bumps [mime](https://github.com/broofa/node-mime) from 2.4.3 to 2.4.4.
- [Release notes](https://github.com/broofa/node-mime/releases)
- [Changelog](https://github.com/broofa/node-mime/blob/master/CHANGELOG.md)
- [Commits](https://github.com/broofa/node-mime/compare/v2.4.3...v2.4.4)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-07-23 12:10:12 +02:00
dependabot-preview[bot] 03f9c6a26e Bump d3 from 5.9.2 to 5.9.7 in /app (#169)
Bumps [d3](https://github.com/d3/d3) from 5.9.2 to 5.9.7.
- [Release notes](https://github.com/d3/d3/releases)
- [Changelog](https://github.com/d3/d3/blob/master/CHANGES.md)
- [Commits](https://github.com/d3/d3/compare/v5.9.2...v5.9.7)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-07-23 12:09:58 +02:00
dependabot-preview[bot] ae7ecd4bc9 Bump webpack-cli from 3.3.4 to 3.3.6 in /app (#170)
Bumps [webpack-cli](https://github.com/webpack/webpack-cli) from 3.3.4 to 3.3.6.
- [Release notes](https://github.com/webpack/webpack-cli/releases)
- [Changelog](https://github.com/webpack/webpack-cli/blob/v3.3.6/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-cli/compare/v3.3.4...v3.3.6)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-07-23 12:09:08 +02:00
dependabot-preview[bot] 25469936fa Bump @types/node from 12.0.8 to 12.6.8 in /app (#167)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 12.0.8 to 12.6.8.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-07-23 12:08:25 +02:00
dependabot-preview[bot] bc2d066bb9 Bump @types/react from 16.8.20 to 16.8.23 in /app (#168)
Bumps [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) from 16.8.20 to 16.8.23.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-07-23 12:07:58 +02:00
dependabot-preview[bot] 71e376e0e4 Bump compare-versions from 3.4.0 to 3.5.0 in /app (#165)
Bumps [compare-versions](https://github.com/omichelsen/compare-versions) from 3.4.0 to 3.5.0.
- [Release notes](https://github.com/omichelsen/compare-versions/releases)
- [Changelog](https://github.com/omichelsen/compare-versions/blob/master/CHANGELOG.md)
- [Commits](https://github.com/omichelsen/compare-versions/compare/v3.4.0...v3.5.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-07-23 12:07:35 +02:00
dependabot-preview[bot] 354b014245 Bump @types/node from 12.0.8 to 12.6.8 (#163)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 12.0.8 to 12.6.8.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-07-23 12:06:25 +02:00
dependabot-preview[bot] d69779c497 Bump tslint from 5.16.0 to 5.18.0 (#162)
Bumps [tslint](https://github.com/palantir/tslint) from 5.16.0 to 5.18.0.
- [Release notes](https://github.com/palantir/tslint/releases)
- [Changelog](https://github.com/palantir/tslint/blob/master/CHANGELOG.md)
- [Commits](https://github.com/palantir/tslint/compare/5.16.0...5.18.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-07-23 12:06:17 +02:00
dependabot-preview[bot] af1219cda8 Bump mocha from 6.1.4 to 6.2.0 (#161)
Bumps [mocha](https://github.com/mochajs/mocha) from 6.1.4 to 6.2.0.
- [Release notes](https://github.com/mochajs/mocha/releases)
- [Changelog](https://github.com/mochajs/mocha/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mochajs/mocha/compare/v6.1.4...v6.2.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2019-07-23 12:06:07 +02:00
Thomas Nordquist c7ea6790c8 Update electron-builder 2019-07-23 01:07:50 +02:00
Thomas Nordquist 779f1e9769 Prepare pre-release 2019-07-23 00:52:59 +02:00
Thomas Nordquist f690b3f5a7 Add recusrive delete warning 2019-07-23 00:51:26 +02:00
Thomas Nordquist 39d87d5a8e Remove recursive topic removal limit 2019-07-23 00:36:15 +02:00
Thomas Nordquist 6781170b85 Fix crash reporter url 2019-07-20 15:51:41 +02:00
Thomas Nordquist 7829a27c69 Bump version to v0.3.5 2019-07-18 00:06:35 +02:00
Thomas Nordquist 285001d184 Bump version 2019-07-17 23:33:41 +02:00
Thomas Nordquist d5a4d1e4d3 Fix dialog button margin 2019-07-17 23:32:52 +02:00
Thomas Nordquist c59c64b65f Fix typing 2019-07-17 20:07:25 +02:00
Thomas Nordquist 5badd19015 Add syntax highlighting to raw / json values 2019-07-17 19:49:10 +02:00
Thomas Nordquist a7eacfac46 Upgrade electron 2019-07-17 19:14:00 +02:00
Thomas Nordquist 20dae2ea2b Fix false dev dependency 2019-07-17 19:07:33 +02:00
Thomas Nordquist dc50fd40a3 Upgrade electron-updater 2019-07-17 17:46:48 +02:00
Thomas Nordquist 3df4d18a71 Update OSX image 2019-07-17 17:45:57 +02:00
Thomas Nordquist 3aeee31193 Update electron-builder 2019-07-17 17:44:51 +02:00
Thomas Nordquist af4cfec451 Auto-open publish history drawer 2019-07-17 17:25:12 +02:00
Thomas Nordquist ae915a158b Add memoization 2019-07-17 16:44:37 +02:00
Thomas Nordquist eac522fe63 Improve publish button layout 2019-07-17 16:35:47 +02:00
Thomas Nordquist a2bdf71422 Fix syntax error 2019-07-17 15:58:21 +02:00
Thomas Nordquist f2d632959b Fix broker statistic updates 2019-07-17 15:56:38 +02:00
Thomas Nordquist b33412504d Unset CSC_LINK for windows builds 2019-07-17 15:44:41 +02:00
dependabot[bot] c458f6c64d Bump lodash from 4.17.11 to 4.17.14 in /app (#158)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.11 to 4.17.14.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.11...4.17.14)

Signed-off-by: dependabot[bot] <support@github.com>
2019-07-17 15:20:51 +02:00
Thomas Nordquist d6489563b2 Increase max title length 2019-07-17 15:20:16 +02:00
Thomas Nordquist d651fc7a6c Refactor value renderer 2019-07-17 15:18:27 +02:00
Thomas Nordquist c86659f61c Fix case-sensitivity error 2019-07-17 14:29:34 +02:00
Thomas Nordquist 8959bbdee7 Always show chart button in history 2019-07-17 14:14:04 +02:00
Thomas Nordquist 9a8474e3d8 Make previous messages more accessible 2019-07-17 14:02:08 +02:00
Thomas Nordquist c5e0e652f3 Improve demo video 2019-07-17 13:33:34 +02:00
Thomas Nordquist f047f97e44 Fix tests 2019-07-17 13:25:44 +02:00
Thomas Nordquist cda8e928ef Bump version to v0.3.3 2019-07-17 13:19:15 +02:00
Thomas Nordquist ccbea4acc5 Fix test 2019-07-17 13:17:41 +02:00
Thomas Nordquist 8ae1528064 Add clear chart button and improve chart menu look&feel 2019-07-17 12:59:25 +02:00
Thomas Nordquist 1c52aced63 Fix button layout 2019-07-17 11:16:03 +02:00
Thomas Nordquist 72020b02b8 Update sidebar when topic is deleted 2019-07-17 10:05:05 +02:00
Thomas Nordquist aa340b2158 Fix topic deletion child topic count 2019-07-17 10:04:20 +02:00
Thomas Nordquist faf909654b Update electron 2019-07-17 09:21:13 +02:00
Thomas Nordquist 33f3458e38 Fix treeNode enhancer 2019-07-17 09:18:06 +02:00
Thomas Nordquist 3737410563 Refactor TreeNode model 2019-07-17 09:17:46 +02:00
Thomas Nordquist 28afc439ba Enable topic deletion with delete key 2019-07-17 09:16:24 +02:00
Thomas Nordquist 04ba067775 Refactor TreeNode 2019-07-17 09:15:25 +02:00
Thomas Nordquist 0a5d010827 Fix layout issue 2019-07-17 09:14:32 +02:00
Thomas Nordquist 1cef529ac7 Add confirmation dialog 2019-07-17 09:14:10 +02:00
Thomas Nordquist 094831f037 Fix highlight color in dark mode 2019-07-17 09:12:09 +02:00
dependabot[bot] 5148e92651 Bump lodash from 4.17.11 to 4.17.14 (#154)
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.11 to 4.17.14.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.11...4.17.14)

Signed-off-by: dependabot[bot] <support@github.com>
2019-07-13 16:21:49 +02:00
dependabot[bot] 13be7a6697 Bump lodash.merge from 4.6.1 to 4.6.2 (#151)
Bumps [lodash.merge](https://github.com/lodash/lodash) from 4.6.1 to 4.6.2.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/commits)

Signed-off-by: dependabot[bot] <support@github.com>
2019-07-13 16:19:43 +02:00
dependabot[bot] 1bef73f356 Bump lodash-es from 4.17.11 to 4.17.14 in /app (#152)
Bumps [lodash-es](https://github.com/lodash/lodash) from 4.17.11 to 4.17.14.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.11...4.17.14)

Signed-off-by: dependabot[bot] <support@github.com>
2019-07-13 16:19:27 +02:00
Thomas Nordquist 88cc57b8b1 Add .github/Funding 2019-07-13 16:15:09 +02:00
Thomas Nordquist b5d9ff3067 Refactor chart 2019-07-13 15:57:30 +02:00
Thomas Nordquist 7c63e871f0 Fix disconnect button label 2019-07-13 14:33:12 +02:00
Thomas Nordquist fd2c78eea0 Fix sidebar layout 2019-07-13 14:22:42 +02:00
Thomas Nordquist e8e8926757 Fix chart tooltip 2019-07-13 14:22:33 +02:00
Thomas Nordquist 0b6a1e42d6 Improve payload truncation in tree 2019-07-13 12:40:47 +02:00
Thomas Nordquist ff148a8e3c Improve tooltip behavior of charts 2019-07-12 17:05:35 +02:00
Thomas Nordquist ecdecd8626 Update gitignore 2019-07-11 22:09:16 +02:00
Thomas Nordquist 843f2377f0 Package mac dmg first 2019-07-11 22:08:15 +02:00
Thomas Nordquist a9a5fdf9dd Fix typo 2019-07-11 22:07:56 +02:00
Thomas Nordquist a9742ebf81 Replace PureComponent performance wrapper 2019-07-11 22:06:07 +02:00
Thomas Nordquist df3e0fc047 Refactor charts 2019-07-11 22:05:02 +02:00
Thomas Nordquist 05867dab48 Fix plot pause function 2019-07-11 18:11:26 +02:00
Thomas Nordquist 4f3150d0f3 Add RingBuffer compactation 2019-07-11 18:10:42 +02:00
Thomas Nordquist 175aeea468 Propagate settings in the tree 2019-07-11 17:14:43 +02:00
Thomas Nordquist 167f0b39e0 Do not indicate topic update on selection 2019-07-11 17:09:37 +02:00
Thomas Nordquist 7a9b7d7bf6 Fix test 2019-07-11 16:34:39 +02:00
Thomas Nordquist 1a1596ad3d Fix tree-rendering performance 2019-07-11 16:14:46 +02:00
Thomas Nordquist 8e49d19fbe Refactor 2019-07-11 15:33:42 +02:00
Thomas Nordquist df42d75651 Refactor 2019-07-11 13:54:53 +02:00
Thomas Nordquist c1e2a4c625 Improve render speed 2019-07-08 17:17:46 +02:00
Thomas Nordquist 901acf2bed Prepare release 2019-07-08 01:42:29 +02:00
Thomas Nordquist 4ec7c8ca75 Revert "Update material-ui"
This reverts commit 75c3619898.
2019-07-08 01:41:42 +02:00
Thomas Nordquist 75c3619898 Update material-ui 2019-07-08 01:31:27 +02:00
Thomas Nordquist 38f8d2e6ee Improve render performance 2019-07-08 01:22:16 +02:00
Thomas Nordquist e3584add7c Fix broker stats 2019-07-08 00:19:34 +02:00
Thomas Nordquist 77dcbccd5c Add time range support to charts 2019-07-07 23:54:28 +02:00
Thomas Nordquist 0ff6359a41 Allow scrolling in profile list 2019-07-07 23:52:51 +02:00
Thomas Nordquist 66bfcab256 Fix test 2019-07-07 23:11:13 +02:00
Thomas Nordquist 195dcf37d4 Add time range setting for charts 2019-07-07 22:14:49 +02:00
Thomas Nordquist 5830d99d45 Quit ui-tests eary if an error occurs 2019-07-07 16:50:29 +02:00
Thomas Nordquist d50cef7bb3 Repair reducer 2019-07-07 16:24:53 +02:00
Thomas Nordquist b79725bdf0 Refactor sidebar 2019-07-07 16:24:18 +02:00
Thomas Nordquist 6bead5b5a6 Fix animation glitch 2019-07-07 12:59:30 +02:00
Thomas Nordquist 183ea9d8c0 Allow publishing topics with ctrl+return 2019-07-07 12:44:34 +02:00
Thomas Nordquist 282736d2f6 Use default domain if plot data is empty 2019-07-07 12:31:07 +02:00
Thomas Nordquist 45b30e5997 Update dependencies 2019-07-07 12:04:27 +02:00
Thomas Nordquist 3d45e8ce6e Disable code signing for windows 2019-07-07 11:52:36 +02:00
Thomas Nordquist 393cf76839 Update dictionary 2019-07-02 15:34:43 +02:00
Thomas Nordquist 410bc6a39b Prepare release 2019-07-02 15:32:04 +02:00
Thomas Nordquist a56c00aeb3 Hotfix certificates selection 2019-07-02 15:27:38 +02:00
Thomas Nordquist c3d7d653fb Update electron-builder 2019-07-02 15:20:33 +02:00
Thomas Nordquist 9d72ef0122 Disable delete key 2019-07-02 15:20:16 +02:00
Thomas Nordquist b5aa22a6d8 Improve render performance 2019-07-02 15:10:46 +02:00
Thomas Nordquist aa05c16651 Report crashes 2019-07-02 13:38:34 +02:00
Thomas Nordquist c543d0fb85 Disable waylanf for snap builds 2019-07-02 11:11:32 +02:00
Thomas Nordquist de7fa2dd9c Fix tests 2019-06-27 10:04:56 +02:00
Thomas Nordquist f9829c2d5c Refactor 2019-06-26 17:41:21 +02:00
Thomas Nordquist e02091f645 Expose model interface 2019-06-26 17:41:21 +02:00
Thomas Nordquist 8d15018986 Refactor TreeNodeComponent 2019-06-26 17:41:21 +02:00
Thomas Nordquist 76c63d38cd Fix type mismatch 2019-06-26 17:41:21 +02:00
Thomas Nordquist 188b5c6c16 Fix typical bugs 2019-06-26 17:41:21 +02:00
greenkeeper[bot] fc5a5d2035 Update electron in group default to the latest version 🚀 (#136)
* chore(package): update electron to version 5.0.5

* chore(package): update electron to version 5.0.5

* chore(package): update lockfile yarn.lock
2019-06-25 21:29:50 +02:00
greenkeeper[bot] 838d106b62 Update spectron to the latest version 🚀 (#131)
* chore(package): update spectron to version 6.0.0

* chore(package): update lockfile yarn.lock
2019-06-25 21:29:09 +02:00
Thomas Nordquist d59a1af5d4 Refactor TreeNodeComponent 2019-06-25 17:44:34 +02:00
Thomas Nordquist 1638080e85 Improve keyboard arrow navigation 2019-06-25 14:30:47 +02:00
Thomas Nordquist f4051b4cdf Add tree navigation via arrow keys 2019-06-25 01:39:31 +02:00
Thomas Nordquist d054e64568 Focus on first input element of connection settings 2019-06-24 15:59:02 +02:00
Thomas Nordquist 759176c4fc Improve RangeSettings keyboard compatibility 2019-06-24 15:58:43 +02:00
Thomas Nordquist c2a9b7b1c6 Fix faulty electron version detection 2019-06-24 14:07:32 +02:00
Thomas Nordquist 2536b9fff7 Fix ui glitch 2019-06-24 14:07:15 +02:00
Thomas Nordquist b714547928 Fix typo 2019-06-24 12:58:05 +02:00
Thomas Nordquist 005fba5ca3 Prevent tab-event closing the chart range settings 2019-06-24 12:47:22 +02:00
Thomas Nordquist b27df0c0d8 Allow to select connection profile with arrow keys 2019-06-24 12:27:15 +02:00
Thomas Nordquist 341ab44f9e Refactor ProfileList 2019-06-24 12:10:02 +02:00
Thomas Nordquist e0d35548c5 Improve "search as you type" feature 2019-06-24 11:39:24 +02:00
Thomas Nordquist ba91730f43 Add support for keyboard events 2019-06-24 11:22:50 +02:00
Thomas Nordquist f691a7a0ae Add keycodes enum 2019-06-23 12:31:08 +02:00
Thomas Nordquist 5a43ecd8d2 Add dramatic pause to video 2019-06-23 12:30:53 +02:00
Thomas Nordquist 73f33c31bb Remove wrapper script from snaps 2019-06-23 12:30:35 +02:00
Thomas Nordquist ffdce6f825 Add keyboard events enter/escape to connect/disconnect 2019-06-23 12:29:52 +02:00
Thomas Nordquist 38b16daf51 Add client certificates 2019-06-21 01:59:38 +02:00
Thomas Nordquist bed5c74150 Prepare SNI test release 2019-06-20 18:34:34 +02:00
Thomas Nordquist 261454986e Add SNI support 2019-06-20 18:18:44 +02:00
Thomas Nordquist 03784601ea Remove snap repackaging 2019-06-20 14:01:33 +02:00
Thomas Nordquist b060861657 Increase test resiliance 2019-06-20 12:44:02 +02:00
Thomas Nordquist 34a117b74c Update dependencies 2019-06-19 12:15:40 +02:00
Thomas Nordquist 9e341afdcc Add cspell 2019-06-19 12:06:23 +02:00
Thomas Nordquist 0d585eef8f Fix typos 2019-06-19 11:59:14 +02:00
Thomas Nordquist 7e99c18410 Update cspell dictionary 2019-06-18 23:19:29 +02:00
Thomas Nordquist 68118248c9 Improve ui-test video 2019-06-18 17:26:10 +02:00
Thomas Nordquist f1a2ae16c1 Fix plot theme switch 2019-06-18 17:05:22 +02:00
Thomas Nordquist 7904cb22cf Update @types/node 2019-06-18 16:25:17 +02:00
Thomas Nordquist 0e0fbae8bb Freeze electron package version in package.json 2019-06-18 15:48:50 +02:00
Thomas Nordquist 3b72048b67 Fix linting error 2019-06-18 15:13:44 +02:00
Thomas Nordquist caa54809b4 Show app name on windows tiles 2019-06-18 15:04:39 +02:00
Thomas Nordquist 23f5846122 Prepare release v0.3.0 2019-06-18 14:53:55 +02:00
Thomas Nordquist de7b461b4e Update ui-test video 2019-06-18 14:49:48 +02:00
Thomas Nordquist d92ea0bad7 Remove scene from ui-test video 2019-06-18 14:49:24 +02:00
Thomas Nordquist d7bb4bb55f Fix layout default sizes 2019-06-18 14:48:52 +02:00
Thomas Nordquist 9ed8a3b91e Add ui-test accessibility 2019-06-18 14:48:39 +02:00
Thomas Nordquist dd78b43058 Fix sidebar scroll behavior 2019-06-18 14:48:16 +02:00
Thomas Nordquist cd68e966ab Fix test text input 2019-06-18 10:27:58 +02:00
Thomas Nordquist 6baeafce39 Fix typo 2019-06-18 10:27:42 +02:00
Thomas Nordquist 1d8900d6eb Move chart panel below tree 2019-06-17 23:27:16 +02:00
Thomas Nordquist e82c8c4eb0 Refactor 2019-06-17 21:31:54 +02:00
Thomas Nordquist 449d046ce3 Avoid dotPath confusions 2019-06-17 19:11:58 +02:00
Thomas Nordquist 06ab52bb6b Fix chart panel auto-open 2019-06-17 19:08:40 +02:00
Thomas Nordquist 181575f71f Prepare release v0.3.0-alpha2 2019-06-17 18:29:46 +02:00
Thomas Nordquist 3d5a4af705 Tidy up ui 2019-06-17 18:27:19 +02:00
Thomas Nordquist 4e982be613 Show notification when adding charts 2019-06-17 18:19:26 +02:00
Thomas Nordquist 8c1c6387c9 Add colors and move capability to charts 2019-06-17 18:08:35 +02:00
Thomas Nordquist 0f9c2cd36f Allow custom options for charts 2019-06-17 16:37:13 +02:00
Thomas Nordquist 2296143883 Set fixed border for bottom panel 2019-06-17 16:36:40 +02:00
Thomas Nordquist fcb61b563c Refactor 2019-06-17 12:12:22 +02:00
Thomas Nordquist 90e5336c5c Filter tracking 2019-06-17 12:11:24 +02:00
Thomas Nordquist 37d56e24ac Fix chart tree source 2019-06-16 23:06:32 +02:00
Thomas Nordquist 57f979e1bb Prepare release v0.3.0-alpha 2019-06-16 22:43:37 +02:00
Thomas Nordquist 00cbb63181 Add ShowPlot icon/action to MessageHistory 2019-06-16 22:43:13 +02:00
Thomas Nordquist eaafb68117 Throttle chart updates 2019-06-16 22:42:17 +02:00
Thomas Nordquist 486a3568e9 Allow plotting edge-case values 2019-06-16 22:31:21 +02:00
Thomas Nordquist acff7ea631 Refactor 2019-06-16 22:30:56 +02:00
Thomas Nordquist 6829046d8c Disable reducer tracking 2019-06-16 22:30:38 +02:00
Thomas Nordquist 182285d8a5 Fix ui glitch in topic breadcrumbs 2019-06-16 21:56:30 +02:00
Thomas Nordquist 1af799789d Only migrate eclipse mqtt server when not using ssl 2019-06-16 21:29:37 +02:00
Thomas Nordquist 7036d6c8fe Migrate from iot.eclipse.org to mqtt.eclipse.org 2019-06-16 21:23:22 +02:00
Thomas Nordquist 0b067bd964 Update electron to 5.0.4 2019-06-16 21:01:08 +02:00
Thomas Nordquist 9fe5133ef1 Improve plot performance with memoization 2019-06-16 20:55:55 +02:00
Thomas Nordquist c9aae4287c Fix chart panel auto-open 2019-06-16 20:50:18 +02:00
Thomas Nordquist 97846e01a7 Use mui-theme to color plots 2019-06-16 20:18:48 +02:00
Thomas Nordquist b4b6c214cb Open/close chart panel automatically 2019-06-16 20:18:26 +02:00
Thomas Nordquist 1d77af5e22 Improve message receive time accuracy 2019-06-16 19:39:40 +02:00
Thomas Nordquist 1884f3baae Fix code style 2019-06-16 19:25:50 +02:00
Thomas Nordquist 0ebb6f4424 Round time intervals 2019-06-16 19:23:24 +02:00
Thomas Nordquist 209899c3b8 Add numeric chart panel 2019-06-16 19:10:37 +02:00
Thomas Nordquist 4ec8cdf0ff Fix linting on windows 2019-06-15 20:27:30 +02:00
Thomas Nordquist 53326b6de4 Fix linter error 2019-06-15 20:11:35 +02:00
Thomas Nordquist 8c13282a55 Fix axios dependecy 2019-06-15 17:23:14 +02:00
Thomas Nordquist 0e3d94bf06 Remove ffmpreg-concat from build 2019-06-15 16:37:24 +02:00
Thomas Nordquist af8613096d Add automatic linting 2019-06-15 15:04:15 +02:00
Thomas Nordquist 92e045297e Update code formatting 2019-06-15 14:56:57 +02:00
Thomas Nordquist 6176859c7c Update MaterialUI 2019-06-14 11:58:46 +02:00
Thomas Nordquist 3935b1d614 Fix flaky diagram 2019-06-14 10:41:52 +02:00
greenkeeper[bot] c9d3e552ae Update css-loader in group default to the latest version 🚀 (#125)
* chore(package): update css-loader to version 3.0.0

* chore(package): update lockfile app/yarn.lock
2019-06-12 18:34:49 +02:00
Thomas Nordquist d62305303a Refactor 2019-06-06 23:20:20 +02:00
Thomas Nordquist 34c81f7f69 Extract style 2019-06-06 23:16:40 +02:00
Thomas Nordquist bd1f6b6d82 Resolve error when trying to decode faulty json 2019-06-06 23:16:40 +02:00
Thomas Nordquist e728a721aa Prevent unneccessary re-renders 2019-06-06 23:16:40 +02:00
greenkeeper[bot] 15a0c8a5c5 Update file-loader in group default to the latest version 🚀 (#122)
* fix(package): update file-loader to version 4.0.0

* chore(package): update lockfile app/yarn.lock
2019-06-06 09:36:12 +02:00
Thomas Nordquist 173f662fa7 Fix readme 2019-06-05 13:49:35 +02:00
Thomas Nordquist 15c4dc2df3 Fix binary switches for settings drawer 2019-06-05 13:28:07 +02:00
Thomas Nordquist b9dcc4a9a8 Fix diagram mouse event handlers 2019-06-05 13:22:12 +02:00
greenkeeper[bot] f8648b92a1 Update dependencies to enable Greenkeeper 🌴 (#121)
* chore: add Greenkeeper config file

* chore(package): update dependencies

* chore(package): update dependencies

* docs(readme): add Greenkeeper badge

* chore(package): update lockfile app/yarn.lock

* chore(package): update lockfile yarn.lock

* Fix update specific issues

* Fix AceEditor
2019-06-05 11:48:15 +02:00
Thomas Nordquist 4f0b13bdc7 Clean up 2019-06-04 23:03:53 +02:00
Thomas Nordquist 1f9215a19f Fix ui-tests 2019-06-04 23:00:42 +02:00
Thomas Nordquist 9e15e28147 Add JSON based plots 2019-06-04 21:51:23 +02:00
Thomas Nordquist 09151d14a9 Handle invalid json
- fix style
2019-06-04 14:36:28 +02:00
Thomas Nordquist e66f6d098a Refactor CodeDiff 2019-06-04 11:32:59 +02:00
Thomas Nordquist 8cc3d416b8 Update axios 2019-06-04 10:48:13 +02:00
Thomas Nordquist 348b72ea69 Fix type error 2019-06-04 10:45:02 +02:00
Thomas Nordquist 050ba81760 Prepare json literal inpsection 2019-05-28 17:49:44 +02:00
Thomas Nordquist fe16eabb6b Adapt to newer webdriver api 2019-05-28 11:00:06 +02:00
Thomas Nordquist 3c58164539 Fix heapdump rebuild script 2019-05-28 10:57:57 +02:00
Thomas Nordquist 5a9d6aa4dd Remove depreacted lockfile 2019-05-28 10:57:35 +02:00
Thomas Nordquist 5c413a9ebb Update dependencies of web-app 2019-05-28 10:47:17 +02:00
Thomas Nordquist d0ddec746a Update dependecies 2019-05-28 10:42:33 +02:00
Thomas Nordquist 6e4754f475 Disable auto-updater for appx/snap/mas builds 2019-05-07 17:44:42 +02:00
Thomas Nordquist 63f89d628e Destroy view-models when destroying trees 2019-05-07 17:43:31 +02:00
Thomas Nordquist dfaae34cf5 Add bigger microsoft store icon 2019-05-07 13:16:19 +02:00
Thomas Nordquist 77654c7136 Bump version 2019-05-04 13:05:12 +02:00
Thomas Nordquist 3bfc59d89a Fix silent error in treenode 2019-05-04 13:03:36 +02:00
Thomas Nordquist 6bf84055d9 Update images in readme 2019-05-03 12:11:03 +02:00
Thomas Nordquist 12d7d3ab64 Fix download location 2019-05-03 12:07:58 +02:00
Thomas Nordquist b2913c7e0e Disable auto update for osx only 2019-05-03 12:07:58 +02:00
Thomas Nordquist 352948f212 Fix update notifier url 2019-05-03 12:07:58 +02:00
Thomas Nordquist 9a4dbe92a0 Fix connection health indicator in connection manager 2019-05-03 12:07:58 +02:00
Thomas Nordquist 397b95f9e5 Disable auto update
Auto update is buggy https://github.com/electron-userland/electron-builder/issues/3681
2019-05-03 12:07:58 +02:00
Thomas Nordquist a411d21133 Fix auto update 2019-05-03 12:07:58 +02:00
Thomas Nordquist 4f624cc130 Fix download url 2019-05-03 12:07:58 +02:00
Thomas Nordquist f10bfafa1d Delete me 2019-05-03 12:07:58 +02:00
Thomas Nordquist 10e37624a5 Improve demo video 2019-05-03 12:07:58 +02:00
Thomas Nordquist ebdfac39eb Fix copy to clipboard layout glitch 2019-05-03 12:07:58 +02:00
Thomas Nordquist 7cc032cdf4 Add afterPack script 2019-05-03 02:07:34 +02:00
Thomas Nordquist 4bc9039386 Fix sed command 2019-05-03 00:59:29 +02:00
Thomas Nordquist 43b997fb18 Add wrapper script to disable sandbox 2019-05-03 00:59:08 +02:00
Thomas Nordquist 6c883aa226 Disable sandbox for snap builds 2019-05-03 00:16:30 +02:00
Thomas Nordquist 20cde3e2a4 Merge branch 'master' of https://github.com/thomasnordquist/MQTT-Explorer 2019-05-01 02:46:06 +02:00
Thomas Nordquist ae7440f8ab Fix linux icon 2019-05-01 01:26:00 +02:00
Thomas Nordquist 03e228e5a0 Fix ci fullscreen flag 2019-05-01 00:58:04 +02:00
Thomas Nordquist 14f2f9ff1e Fix mac release artifact publish settings 2019-04-30 21:22:54 +02:00
Thomas Nordquist 86544c1334 Disable sandbox 2019-04-30 18:35:29 +02:00
Thomas Nordquist 9e71c3132e Use proper provisioning profile for OSX builds 2019-04-30 18:28:35 +02:00
Thomas Nordquist 817202fc20 Disable electron sandbox due to crashes 2019-04-30 17:44:53 +02:00
Thomas Nordquist 931ec0113c Fix ui glitch 2019-04-30 17:44:25 +02:00
Thomas Nordquist 365ebc78ab Fix ui tests 2019-04-30 16:25:37 +02:00
Thomas Nordquist 2a541a80dc Fix ui tests 2019-04-30 15:54:53 +02:00
Thomas Nordquist a1d3f32f73 Fix mouse-dummy pointer events for demo video 2019-04-27 10:32:45 +02:00
Thomas Nordquist e9e6ea618d Change leak acceptance level 2019-04-27 10:31:16 +02:00
Thomas Nordquist 6021df7150 Fix minor ui-bugs 2019-04-27 10:31:06 +02:00
Thomas Nordquist 070b72b304 Document license 2019-04-27 10:29:37 +02:00
Thomas Nordquist 1f65a0f316 Upgrade electron to v5.0.0 2019-04-26 17:45:35 +02:00
Thomas Nordquist 0f21f10c0d Fix backend tests 2019-04-26 14:57:12 +02:00
Thomas Nordquist af2ff0149d Add memory leak test-suite 2019-04-25 13:50:15 +02:00
Thomas Nordquist 8cd11cde3b Disable redux dev tools on default 2019-04-25 09:01:07 +02:00
Thomas Nordquist fdbe6344d9 Disable diff hover highlight 2019-04-25 09:00:44 +02:00
Thomas Nordquist a56b41635c Fix indentation 2019-04-25 00:28:48 +02:00
Thomas Nordquist 2c5c218fd1 Remove unsubscription since component would unount when node changes 2019-04-25 00:05:33 +02:00
Thomas Nordquist 749df70d5c Fix memory leaks 2019-04-24 23:40:28 +02:00
Thomas Nordquist 4c4e1543ec Prepare release 2019-04-17 19:32:43 +02:00
Thomas Nordquist 9320fa252e Add license 2019-04-17 19:32:29 +02:00
Thomas Nordquist 9062129114 Fix themes 2019-04-17 19:21:32 +02:00
Thomas Nordquist fa3805460c Fix ValueRenderer button / time positioning 2019-04-16 12:48:50 +02:00
Thomas Nordquist c2885c4829 Add time locale selection 2019-04-16 12:48:10 +02:00
Thomas Nordquist a901f2b90b Remove ffmpeg-concat to fix windows build 2019-04-16 08:55:23 +02:00
Thomas Nordquist 349b664270 Add auto-expand steps 2019-04-16 00:51:27 +02:00
Thomas Nordquist a28fc958d4 Fix IconButton shapes and positioning
Fixes #106
2019-04-15 22:40:16 +02:00
Thomas Nordquist 0ec539854c Add comprehendable description for auto-expand
Fixes #102
2019-04-15 22:37:30 +02:00
Thomas Nordquist 9c5d89e0aa Fix ui-glitch
Fixes #105
2019-04-15 17:07:02 +02:00
Thomas Nordquist c9c0a447ce Fix removal of empty topics
Fixes #104
2019-04-15 16:58:50 +02:00
Thomas Nordquist 38819b0e0a Deselect message history message when topic selection changes
Fixes #93
2019-04-15 16:45:58 +02:00
Thomas Nordquist d24dc41024 Increase buffer sizes 2019-04-15 16:19:38 +02:00
Thomas Nordquist 62774fe9c2 Fixx CodeDiff scroll behaviour 2019-04-15 16:17:41 +02:00
Thomas Nordquist 4b15650290 Use better source-map 2019-04-15 15:54:18 +02:00
Thomas Nordquist 64e974f6c7 Remove unused prop 2019-04-15 15:54:10 +02:00
Thomas Nordquist fb137a703c Fix CodeDiff scrolling 2019-04-15 15:53:51 +02:00
Thomas Nordquist c43d1fadb4 Fix initial topic selection for Publish component
Fixes #101
2019-04-15 15:52:43 +02:00
Thomas Nordquist 91a9ba7757 Refactor CustomIconButtons 2019-04-15 15:52:27 +02:00
Thomas Nordquist 48e65947f7 Load (my) redux dev tools in dev mode 2019-04-15 14:15:59 +02:00
Thomas Nordquist 7f29dadc42 Fix down-scale behavior 2019-04-15 14:15:06 +02:00
Thomas Nordquist 47d1e74852 Reset store after disconnect 2019-04-15 14:14:43 +02:00
Thomas Nordquist 6d6b35d5f8 Fix react warnings 2019-04-15 11:55:55 +02:00
Thomas Nordquist 0d56ab49ea Fix sidebar topic theme 2019-04-15 11:43:07 +02:00
Thomas Nordquist daac8cf949 Fix value preview update 2019-04-15 11:35:13 +02:00
Thomas Nordquist fa54b9a9fa Align value preview style with code editor 2019-04-15 11:34:40 +02:00
Thomas Nordquist d339d6a1bb Remove dead code 2019-04-15 11:05:38 +02:00
Thomas Nordquist b6f16c347d Reduce selectable elements 2019-04-15 11:03:58 +02:00
Thomas Nordquist 46fa93edbd Fix resizer highlight and scroll bar style 2019-04-15 11:00:28 +02:00
Thomas Nordquist 2de7840897 Script ui-test scenes 2019-04-15 10:15:06 +02:00
Thomas Nordquist 499dfd1b68 Fix history scroll behavior and text selection
Related to #92
2019-04-14 21:39:13 +02:00
Thomas Nordquist 425bbb36e3 Rename HistoryDrawer 2019-04-14 20:37:57 +02:00
Thomas Nordquist b8f510bd7e Select message content with Ctrl + A if message is in focus
Fixes #92
2019-04-14 20:24:51 +02:00
Thomas Nordquist aae1381de5 Removed outline from selected elements 2019-04-14 20:24:36 +02:00
Thomas Nordquist 266d337dae Upgrade electron 2019-04-13 12:50:31 +02:00
Thomas Nordquist e6bcc00927 Record tests as separate scenes 2019-04-13 12:49:47 +02:00
Thomas Nordquist d1f4bc678c Fix code-style 2019-04-11 20:41:08 +02:00
Thomas Nordquist 52c44327d0 Move UpdateNotifier 2019-04-11 11:49:28 +02:00
Thomas Nordquist 50b2362a70 Allow pre-releases if own version is a beta 2019-04-10 20:43:34 +02:00
Thomas Nordquist fb8a16e1d7 Fix update notification 2019-04-10 20:34:44 +02:00
Thomas Nordquist a929729e6a Use file loader to load demo cursor 2019-04-10 20:34:27 +02:00
Thomas Nordquist b12961cbc9 Fix codestyle 2019-04-10 20:25:52 +02:00
Thomas Nordquist f0465a8ad7 Document theme color proposals 2019-04-10 20:17:25 +02:00
Thomas Nordquist 869ad9eb37 Remove \”Loading...\” suspense fallback 2019-04-10 20:16:49 +02:00
Thomas Nordquist dcff2ae336 Move settings visibility to global state
Fixes #95
2019-04-10 20:16:22 +02:00
Thomas Nordquist b6d6575543 Refactor reducers 2019-04-10 19:47:55 +02:00
Thomas Nordquist a2fae27919 Refactor Settings component 2019-04-10 19:47:20 +02:00
Thomas Nordquist 14f0f32560 Fix setTheme action signature 2019-04-10 18:55:47 +02:00
Thomas Nordquist 195c2db70d Fix ClearAdonment for light theme 2019-04-10 18:55:25 +02:00
Thomas Nordquist 8773d73ead Don't save autoExpandLimit 2019-04-10 18:54:39 +02:00
Thomas Nordquist eab20bc614 Fix topicFilter record initialisation 2019-04-10 14:00:46 +02:00
Thomas Nordquist ab8365b1f7 Fix pause icon alignment 2019-04-10 14:00:02 +02:00
Thomas Nordquist c7b0400e84 Revert "Fix zoom key modifier for windows"
This reverts commit 2590a701bb.
Test failed
2019-04-10 12:05:27 +02:00
Thomas Nordquist 10bac1bcd6 Fix topic expansion test helper 2019-04-10 11:12:27 +02:00
Thomas Nordquist 8a5bd3526d Fix cursor color based on theme 2019-04-10 11:00:25 +02:00
Thomas Nordquist 2590a701bb Fix zoom key modifier for windows
Windows uses Ctrl+= istead of Ctrl++ otherwise
Seen this fix in the signal app
2019-04-10 10:54:01 +02:00
Thomas Nordquist 36b9cc0bea Use prebuilt ffmpeg environment 2019-04-10 10:34:50 +02:00
Thomas Nordquist 5260343700 Fix cleanup snap repackaging 2019-04-10 10:21:58 +02:00
Thomas Nordquist 5be21475ce Revert "Upgrade electron"
This reverts commit cefd9e95db.
2019-04-09 23:56:20 +02:00
Thomas Nordquist 1e8a451a68 Fix test execution 2019-04-09 19:49:40 +02:00
Thomas Nordquist b7f914d936 Prepare alpha2 2019-04-09 19:07:52 +02:00
Thomas Nordquist cefd9e95db Upgrade electron 2019-04-09 19:07:05 +02:00
Thomas Nordquist 4dfeddbdae Fix test execution 2019-04-09 15:53:25 +02:00
Thomas Nordquist 11382e3af6 Fix method name 2019-04-09 12:09:42 +02:00
Thomas Nordquist 1400540852 Refactor electron launcher
Don't try auto-update on portable builds
2019-04-09 12:04:20 +02:00
Thomas Nordquist d005195cfe Fix tooltip warning 2019-04-09 01:52:16 +02:00
Thomas Nordquist 97a59ea691 Fix cs 2019-04-09 01:51:49 +02:00
Thomas Nordquist 2812baccf9 Merge changes when resuming 2019-04-09 01:51:31 +02:00
Thomas Nordquist f5de7903e7 Fix highlight animation 2019-04-09 01:39:14 +02:00
Thomas Nordquist 9a65e79fc4 Use less subscribers for TreeNode updates 2019-04-09 01:38:48 +02:00
Thomas Nordquist 4598977b61 Decode base64 messages early 2019-04-09 00:41:56 +02:00
Thomas Nordquist c461121d6d Fix indentation 2019-04-09 00:04:51 +02:00
Thomas Nordquist fcc560ef3f Set keys in TreeNodeTitle 2019-04-09 00:04:36 +02:00
Thomas Nordquist f76ea848cd Reduce tree DOM nodes by 33% 2019-04-08 20:23:07 +02:00
Thomas Nordquist 431d0c89fe Remove dead code 2019-04-08 16:53:09 +02:00
Thomas Nordquist 083a39d6c6 Remove remark 2019-04-08 14:16:24 +02:00
Thomas Nordquist 7e5e6013ab Fix ValueRenderer code block theme 2019-04-08 14:12:20 +02:00
Thomas Nordquist acbe20070b Prevent selection if typographics 2019-04-08 14:00:22 +02:00
Thomas Nordquist e89ca2bfa4 Improve development
Redirect app visitors to mqtt-explorer.com
2019-04-08 13:59:45 +02:00
Thomas Nordquist 96da5badd4 Inhibit text previe wmode delselection
Fixes #88
2019-04-08 13:48:44 +02:00
Thomas Nordquist e9734bdfc5 Refactor ValueRenderer action and meta info layout 2019-04-08 13:43:07 +02:00
Thomas Nordquist f1d9a82057 Move eslitrc 2019-04-08 09:54:27 +02:00
Thomas Nordquist 550d7014fa Extract theme creation 2019-04-08 09:37:14 +02:00
Thomas Nordquist db134dbc1c Fix font-size layout issues 2019-04-08 09:27:25 +02:00
Thomas Nordquist 7e38d23475 Fix ui-glitch in pause tooltip 2019-04-08 09:00:56 +02:00
Thomas Nordquist c23099e254 Refactor notification 2019-04-08 09:00:23 +02:00
Thomas Nordquist e4cfa38139 Remove live-reload-plugin in favor of hmr 2019-04-08 08:54:10 +02:00
Thomas Nordquist e9ab3da6c2 Disable bundle analyzer for ci builds 2019-04-08 08:51:39 +02:00
Thomas Nordquist a150da0254 Fix save & load settings 2019-04-08 00:57:53 +02:00
Thomas Nordquist 8c5f708386 Add notification when merging changes into the tree 2019-04-08 00:57:32 +02:00
Thomas Nordquist 436b569e93 Fix updating tree nodes when settings change 2019-04-07 22:56:10 +02:00
Thomas Nordquist 3e47b07ba7 Fix js-base64 include 2019-04-07 22:55:01 +02:00
Thomas Nordquist da5e58a417 Add .eslintrc 2019-04-07 21:52:29 +02:00
Thomas Nordquist c993bd88e0 Fix icon generation script 2019-04-07 21:40:42 +02:00
Thomas Nordquist 5c038461a6 Fix implicit dependecy 2019-04-07 21:36:20 +02:00
Thomas Nordquist e2c60cca64 Refactor project structure 2019-04-07 21:34:03 +02:00
Thomas Nordquist 16c72fa9be Upgrade dependecies 2019-04-07 20:01:04 +02:00
Thomas Nordquist 8571d97182 Refactor 2019-04-07 19:44:09 +02:00
Thomas Nordquist c7e20c26cb Extract ChangeBuffer to own file 2019-04-07 19:32:13 +02:00
Thomas Nordquist ab3e35520b Move gh-pages related stuff 2019-04-05 16:52:13 +02:00
Thomas Nordquist 7a78cffa13 Use development env 2019-04-05 16:50:19 +02:00
Thomas Nordquist 09dcce97b7 Refactor 2019-04-04 20:23:27 +02:00
Thomas Nordquist c20c075bcf Fix linter errors 2019-04-04 10:54:18 +02:00
Thomas Nordquist d175195dd5 Fix case-sensitive path 2019-04-03 18:27:38 +02:00
Thomas Nordquist 094d795b39 Add pause feature 2019-04-03 17:57:42 +02:00
Thomas Nordquist 8266a87417 Fix numeric plots 2019-04-03 17:56:01 +02:00
Thomas Nordquist f23f6c908f Improve message history 2019-04-03 17:55:39 +02:00
Thomas Nordquist 9e5d944df3 Fix connection health in light theme 2019-04-03 17:54:47 +02:00
Thomas Nordquist b5068ad3d5 Fix editor theme 2019-04-03 17:52:50 +02:00
Thomas Nordquist 6853066a19 Add light theme 2019-04-03 06:09:34 +02:00
Thomas Nordquist acbaced1ec Fix tests 2019-04-03 03:37:48 +02:00
Thomas Nordquist b9eb54dd20 Move theme setting to settings store 2019-04-03 02:14:09 +02:00
Thomas Nordquist 6f86a8d471 Add theme toggle 2019-04-03 01:55:57 +02:00
Thomas Nordquist 84a92ad522 Fix base64 encoded string 2019-04-03 01:36:52 +02:00
Thomas Nordquist 5caa3564d1 Remove debugger statement 2019-04-03 01:32:13 +02:00
Thomas Nordquist d3598d8417 Use JSON over strings as payload format 2019-04-03 00:39:14 +02:00
Thomas Nordquist 27f5e8a7eb Rename Value Panel 2019-04-02 19:44:02 +02:00
Thomas Nordquist b97f97de99 Fix typo 2019-04-02 19:31:37 +02:00
Thomas Nordquist 0c124d8d19 Extract value renderer panel 2019-04-02 19:30:21 +02:00
Thomas Nordquist f7e3fbc8f9 Add badge 2019-03-28 13:03:57 +01:00
Thomas Nordquist 100bfdd560 Fix typos 2019-03-26 16:04:32 +01:00
Thomas Nordquist 01f42a1c32 Excluse alpha/beta releases from update notifications 2019-03-26 15:46:26 +01:00
Thomas Nordquist 2cb8c0dabc Update readme 2019-03-26 15:30:48 +01:00
Thomas Nordquist cceeb74d96 Fix client-id 2019-03-26 14:48:18 +01:00
Thomas Nordquist c1bc96da01 Add support to validate self-signed certificates 2019-03-26 14:44:47 +01:00
Thomas Nordquist 89d363fbaa Fix date formatting for cases when no navigator language is not set 2019-03-25 19:48:07 +01:00
Thomas Nordquist 62cbf6547e Properly support time locales 2019-03-25 19:21:10 +01:00
Thomas Nordquist ea7994dad9 Update Readme 2019-03-20 10:52:21 +01:00
Thomas Nordquist c09ea4ae62 Add user-selected.read-only entitlement 2019-03-20 10:52:21 +01:00
Thomas Nordquist 8c23f1f27c Allow to pack dev releases 2019-03-20 10:52:21 +01:00
Thomas Nordquist aa8d066fe1 Change execution order to mitigate exception effects
For unknown reasons the "firstItem" is undefined in very rare cases.
By changing the execution order, the drop will still work even if an exception occurs.
2019-03-20 10:52:21 +01:00
Thomas Nordquist 485e1cffae Fix ringbuffer copy constructor 2019-03-20 10:52:21 +01:00
Thomas Nordquist eb7d7e8955 Create CNAME 2019-03-19 17:40:30 +01:00
Thomas Nordquist 6392959e22 Delete CNAME 2019-03-19 17:24:14 +01:00
Thomas Nordquist 581dba7c01 Create CNAME 2019-03-19 17:22:52 +01:00
Thomas Nordquist ea57c55116 Update sidebar
Add border radius to images
2019-03-19 04:20:27 +01:00
Thomas Nordquist d9fcf59ca4 Update readme 2019-03-19 04:20:16 +01:00
Thomas Nordquist d66ecc6f0f Re-add dmg download to readme 2019-03-09 20:31:44 +01:00
Thomas Nordquist eb6aa998a9 Update Readme.md 2019-03-08 10:28:43 +01:00
Thomas Nordquist ef4dfe3298 Update readme 2019-03-07 13:02:48 +01:00
Thomas Nordquist 354a0cbf84 Update product name depending on package 2019-03-07 10:52:15 +01:00
Thomas Nordquist 94558b9a6f Add icons
Add app name to tiles

Move afterPack hook

Update Microsoft store images
2019-03-07 02:42:22 +01:00
Thomas Nordquist 517bb06fb2 Update readme 2019-03-06 14:20:25 +01:00
Thomas Nordquist cd7e5ed412 Prepare release
AppStore review failed, updated entitlements
2019-03-06 14:10:17 +01:00
Thomas Nordquist 58a6b21bf3 Use kramdown for github page 2019-03-06 14:08:01 +01:00
Thomas Nordquist 1763dc7574 Fix osx build 2019-03-06 13:17:59 +01:00
Thomas Nordquist 5adb2bb5c8 Add site layout 2019-03-05 22:34:29 +01:00
Thomas Nordquist 4665130820 Prepare release 2019-03-05 20:20:44 +01:00
Thomas Nordquist c3efd45a7f Update icons 2019-03-05 20:19:33 +01:00
Thomas Nordquist ad0298f8a0 Add AppStore icons 2019-03-05 16:20:14 +01:00
Thomas Nordquist 6545026796 Fix windows store build 2019-03-05 16:19:30 +01:00
Thomas Nordquist a10a10bcd3 Update bundle settings 2019-03-05 16:03:23 +01:00
Thomas Nordquist 908cc50e60 Prepare release 2019-03-05 14:11:06 +01:00
Thomas Nordquist 1a8f64966c Update readme 2019-03-05 14:10:14 +01:00
Thomas Nordquist 1f70c1ba3c Add app screenshot generation 2019-03-05 14:09:02 +01:00
Thomas Nordquist 197781a4d8 Add codesigning and fix packaging 2019-03-05 12:55:44 +01:00
Thomas Nordquist 38803ccf59 Fix mac package marker 2019-03-05 10:53:16 +01:00
Thomas Nordquist 2148b01ba0 Test snap deployment 2019-03-05 10:53:04 +01:00
Thomas Nordquist b404a4dac1 Fix and upload snaps on travis 2019-03-05 00:29:34 +01:00
Thomas Nordquist 4340515012 Add build info to each package 2019-03-05 00:08:56 +01:00
Thomas Nordquist 5205ed1094 Add snap after-package repack script 2019-03-04 20:15:27 +01:00
Thomas Nordquist 7961a7dd3f Prepare release 2019-03-04 18:56:22 +01:00
Thomas Nordquist f8a2af8554 Hint which messages are compared in diff 2019-03-04 18:23:53 +01:00
Thomas Nordquist d1fb0026b6 Reorder imports 2019-03-04 18:01:20 +01:00
Thomas Nordquist 7e9932de0a Show error when json formatting fails 2019-03-04 17:57:12 +01:00
Thomas Nordquist 0dea1f9d19 Clean up connection on disconnect 2019-03-04 17:56:52 +01:00
Thomas Nordquist d7be46c78e Prevent tree-regeneration on reconnect 2019-03-04 17:56:26 +01:00
Thomas Nordquist e95fd5b37d Codestyle 2019-03-04 17:56:00 +01:00
Thomas Nordquist dee1abb1da Show notification when client got disconnected 2019-03-04 17:55:47 +01:00
Thomas Nordquist 4c84a9b5f6 Fix demo 2019-03-04 17:21:11 +01:00
Thomas Nordquist 016bf5dfcd Rework demo video 2019-03-04 16:13:27 +01:00
Thomas Nordquist 9c15e392d1 Add diff line change indicator 2019-03-03 19:04:15 +01:00
Thomas Nordquist 613c6d3aa7 Fix fomat json button 2019-03-03 16:37:23 +01:00
Thomas Nordquist f10cd89647 Demonstrate json formatting 2019-03-03 16:24:55 +01:00
Thomas Nordquist 6000541c32 Visualize diff function in video 2019-03-03 15:52:34 +01:00
Thomas Nordquist 87a86b8c0c Add json formatting 2019-03-03 06:34:06 +01:00
Thomas Nordquist f81d64cc2f Fix diff 2019-03-03 06:33:40 +01:00
Thomas Nordquist 9b31aeccd8 Add +/- signs to code diff 2019-03-03 03:50:02 +01:00
Thomas Nordquist 692054c540 Add quick preview switch 2019-03-03 02:21:05 +01:00
Thomas Nordquist 91486718d8 Fix electron downgrade 2019-03-03 00:55:15 +01:00
Thomas Nordquist 55dd79aebd Change recording resolution 2019-03-03 00:13:56 +01:00
Thomas Nordquist 7141dfa9fc Downgrade electron 2019-03-02 23:59:52 +01:00
Thomas Nordquist f16d0cd271 Clean dependencies 2019-03-02 23:37:04 +01:00
Thomas Nordquist 838e2fa287 Update electron 2019-03-02 23:21:07 +01:00
Thomas Nordquist b9439c3244 Add switch for diffs 2019-03-02 23:20:02 +01:00
Thomas Nordquist 410855e8a0 Fix app build 2019-03-02 21:23:49 +01:00
Thomas Nordquist 71d606d7cd Fix compile errors 2019-03-02 19:57:29 +01:00
Thomas Nordquist a32810417d Fix store logos 2019-03-02 16:08:40 +01:00
Thomas Nordquist 4fcf767079 Change windows store color theme 2019-02-28 15:50:14 +01:00
Thomas Nordquist 1e6fa11f0f Remove Appveyor deploy rule 2019-02-27 17:23:27 +01:00
Thomas Nordquist 9644952d81 Always build appx 2019-02-27 16:15:44 +01:00
Thomas Nordquist d1cd299132 Add appx store logos 2019-02-27 16:14:32 +01:00
Thomas Nordquist e3539015f7 Add appveyor badge 2019-02-26 23:44:52 +01:00
Thomas Nordquist 874cedb422 Add appveyor support (#75)
* Add appveyor build badge
2019-02-26 23:43:53 +01:00
Thomas Nordquist 63530a41ac Fix topic selector xpath 2019-02-25 17:58:24 +01:00
Thomas Nordquist d3bf5e87e1 Clean up dependencies 2019-02-25 17:30:41 +01:00
Thomas Nordquist 10c6c3cecd Add changelog 2019-02-25 17:08:41 +01:00
Thomas Nordquist 4d8fa72491 Refactor 2019-02-25 17:07:49 +01:00
Thomas Nordquist b9c0a66948 Refactor value rendering 2019-02-25 17:07:29 +01:00
Thomas Nordquist 6b859d31e8 Fix loading client id conenction setting
fixes #71
2019-02-25 16:40:43 +01:00
Thomas Nordquist 4fd328e716 Add zoom menu and shortcuts 2019-02-25 13:52:07 +01:00
Thomas Nordquist b606f14836 Clean imports & modules 2019-02-25 12:04:41 +01:00
Thomas Nordquist ad49c5c819 Update diff view 2019-02-25 11:57:44 +01:00
Thomas Nordquist 749a591465 Fix message diffing 2019-02-24 11:46:35 +01:00
Thomas Nordquist a1b1c92648 Remove mouse-over to select topics 2019-02-24 11:38:00 +01:00
Thomas Nordquist 85c6c4ebbc Fix message history comparison selection 2019-02-23 23:26:52 +01:00
Thomas Nordquist de7c9479c9 Highlight selected history entry 2019-02-23 23:16:47 +01:00
Thomas Nordquist 138f51eb39 Add diff view for received messages 2019-02-23 22:49:42 +01:00
Thomas Nordquist 107ca83882 Remove obsolete import 2019-02-23 22:35:20 +01:00
Thomas Nordquist 839ad531ba Fix RingBuffer array type 2019-02-23 22:35:03 +01:00
Thomas Nordquist 174eb0c767 Fix leaking connections 2019-02-23 22:34:32 +01:00
Thomas Nordquist 953422dcd4 Show travis-ci master build status 2019-02-22 12:08:36 +01:00
Thomas Nordquist a4015141c3 Fix zoomFactor 2019-02-21 10:42:51 +01:00
Thomas Nordquist ca3bd0b22b Fix connection window location 2019-02-20 17:09:47 +01:00
Thomas Nordquist 797d11d58d Update video captions 2019-02-20 15:57:32 +01:00
Thomas Nordquist 8281a852ab Change ui test resolution 2019-02-20 15:54:38 +01:00
Thomas Nordquist e5c3f207d9 Update gif image scale 2019-02-20 15:49:13 +01:00
Thomas Nordquist 2efc8f4dda Record videos in 720p 2019-02-20 15:40:35 +01:00
Thomas Nordquist 60a06d8c82 Update Readme & Add snap store link 2019-02-20 15:35:51 +01:00
Thomas Nordquist 857500f9c9 Update electron builder 2019-02-20 11:27:07 +01:00
Thomas Nordquist 396e6985ce Update readme 2019-02-19 00:33:21 +01:00
Thomas Nordquist 6d75f29f03 Update readme 2019-02-19 00:09:25 +01:00
Thomas Nordquist f8cdf0837e Prepare alpha release 2019-02-18 23:48:58 +01:00
Thomas Nordquist 5bb96233d1 Fix asset upload 2019-02-18 23:48:58 +01:00
Thomas Nordquist f3ff6cde7b Fix about window icon 2019-02-18 23:48:58 +01:00
Thomas Nordquist ec88f9822d Fix tests 2019-02-18 23:48:58 +01:00
Thomas Nordquist 235e81826b Add about window 2019-02-18 23:48:58 +01:00
Thomas Nordquist 615ec17b96 Remove empty topics from tree 2019-02-18 23:48:58 +01:00
Thomas Nordquist ddc801fe93 WiP 2019-02-18 23:48:58 +01:00
Thomas Nordquist 24c6b7e7b3 Clean up 2019-02-18 23:48:58 +01:00
Thomas Nordquist 55ea381b3b WiP 2019-02-18 23:48:58 +01:00
Thomas Nordquist 55fbc642c4 Add connection health indicator 2019-02-18 21:03:27 +01:00
Thomas Nordquist 4b5d023d19 Fix disconnect unsubscribe 2019-02-18 20:36:22 +01:00
Thomas Nordquist e0708a5288 Add author information 2019-02-18 14:54:45 +01:00
Thomas Nordquist d44a6e3159 Add clipboard tooltip 2019-02-18 14:54:29 +01:00
Thomas Nordquist 09afe2a30c Fix title 2019-02-18 14:54:09 +01:00
Thomas Nordquist 160b0b5a04 Persist and restore settings 2019-02-18 13:50:58 +01:00
Thomas Nordquist 590c24a3bd Add "Show Activity" switch 2019-02-18 13:01:22 +01:00
Thomas Nordquist e0978ee64b Optimize recording 2019-02-18 12:26:05 +01:00
Thomas Nordquist 09811eafb7 Update test-recording 2019-02-18 09:59:23 +01:00
Thomas Nordquist 795c410a92 Fix grey start of demo gif 2019-02-17 23:42:23 +01:00
Thomas Nordquist 6c25b42a94 Fix ui-test xpath selector 2019-02-17 23:13:04 +01:00
Thomas Nordquist 9207af0aaa Improve settings storage
- add error reporting
- refactor
2019-02-17 21:02:17 +01:00
Thomas Nordquist 0ad91872a1 Refactor 2019-02-17 18:36:02 +01:00
Thomas Nordquist 8b64818b4c Fix text input update issue 2019-02-17 18:35:08 +01:00
Thomas Nordquist 03462f7ec8 Update React & Material-UI 2019-02-17 17:51:42 +01:00
Thomas Nordquist 3f52944f18 Store settings in lowdb 2019-02-17 17:06:46 +01:00
Thomas Nordquist 1740df6218 Fix legacy connection profile migration 2019-02-17 13:14:02 +01:00
Thomas Nordquist aa32349727 Refactor 2019-02-17 12:55:51 +01:00
Thomas Nordquist 6d81520ff9 Migrate legacy connections 2019-02-17 12:54:51 +01:00
Thomas Nordquist 7d165bb342 Remove hivemq broker 2019-02-17 11:12:07 +01:00
Thomas Nordquist 6f3a5beeaa Fix style 2019-02-17 11:11:52 +01:00
Thomas Nordquist 804c96d041 Preview connection URI 2019-02-17 10:36:56 +01:00
Thomas Nordquist 9c863c8339 Subscribe to configures topics 2019-02-17 10:15:04 +01:00
Thomas Nordquist 3cb89fe502 Show broker stats only if compatible format is used 2019-02-17 09:59:49 +01:00
Thomas Nordquist 1339c1a292 Change expander color 2019-02-17 09:58:09 +01:00
Thomas Nordquist 4b8356632c Refactor 2019-02-17 09:57:54 +01:00
Thomas Nordquist e34a38c1f0 Improve tree node title style 2019-02-17 08:35:25 +01:00
Thomas Nordquist e6ecfde339 Improve stpedded rendering 2019-02-17 08:35:05 +01:00
Thomas Nordquist 688abbd999 Improve look&feel 2019-02-17 00:54:42 +01:00
Thomas Nordquist 5d758c8e6d Fix text overflow 2019-02-16 18:19:40 +01:00
Thomas Nordquist ef6946bdd4 Add default connection profiles 2019-02-16 18:04:40 +01:00
Thomas Nordquist 93ea829987 Add connection profiles (#63)
* Add connection setup

* Refactor

* Fix lifecycle
2019-02-16 05:36:02 -08:00
Thomas Nordquist f316d5699d Improve readme for mobile preview 2019-02-14 11:47:12 +01:00
Thomas Nordquist a58049d678 Update license reference in readme 2019-02-05 12:27:03 +01:00
Thomas Nordquist 873df765a0 Create LICENSE
Delete LICENSE

Create LICENSE
2019-02-05 12:25:55 +01:00
Thomas Nordquist 8fdd4f1f0d Fix scrollbar 2019-02-04 21:41:08 +01:00
Thomas Nordquist b7d194a244 Reduce SplitPane step size 2019-02-04 21:40:29 +01:00
Thomas Nordquist a2a24659de Update readme 2019-02-04 19:25:28 +01:00
Thomas Nordquist 8a18cfd56c Update _config.yml
Add github pages analytics id
2019-02-02 01:12:01 +01:00
Thomas Nordquist 28d19875d5 Update readme 2019-01-30 12:39:00 +01:00
Thomas Nordquist d64e085247 Travis ui tests (#57)
* Prepare travis is tests

* Fix ffmpeg travis source

* Trying xenial

* Move shell scripts

* Upload video assets

* Upload video assets

* Change text input method

* Add ui test docker support

* Fix travis docker build

* Fix asset uploader

* Fix dockerfile

* Update dockerfile

* Change writeText behavior

* Fix type error

* Fix exit codes

* Fix types

* fix upload

* Fix writeText

* Fix argument name

* Add test scenarios

* Enable vnc and change mqtt host
2019-01-30 03:13:19 -08:00
376 changed files with 34707 additions and 14942 deletions
+58
View File
@@ -0,0 +1,58 @@
{
"import": [
"@cspell/dict-typescript/cspell-ext.json"
],
"ignoreRegExpList": [
"import(?:(?:(?:[ \\n\\t]+([^ *\\n\\t\\{\\},]+)[ \\n\\t]*(?:,|[ \\n\\t]+))?([ \\n\\t]*\\{(?:[ \\n\\t]*[^ \\n\\t\"'\\{\\}]+[ \\n\\t]*,?)+\\})?[ \\n\\t]*)|[ \n\\n\\t]*\\*[ \\n\\t]*as[ \\n\\t]+([^ \\n\\t\\{\\}]+)[ \\n\\t]+)from[ \\n\\t]*(?:['\"])([^'\"\\n]+)(['\"])\n",
"^import\\s+(['\"]).*\\1$"
],
"language": "en",
"words": [
"Bbreak",
"nodered",
"goog",
"thomasnordquist",
"nowrap",
"subheader",
"basepath",
"repo",
"hexagonalize",
"pixelize",
"Transistions",
"provisionprofile",
"Nsis",
"webdriverio",
"Appx",
"Hashable",
"clickaway",
"Resizer",
"Subnodes",
"Unmount",
"Monokai",
"plottable",
"snackbar",
"Nordquist",
"debounced",
"mosquitto",
"snapcraft",
"unsquashfs",
"armv",
"livingroom",
"selectall",
"toggledevtools",
"Octo",
"zigbee",
"memoization",
"submenu",
"AGPL",
"Oooooops",
"DEVTOOLS",
"mixins",
"Explorerdmg",
"heapsnapshot",
"noconflict",
"sparkplugb",
"protojson",
"typesafe"
]
}
+64
View File
@@ -0,0 +1,64 @@
{
"name": "MQTT Explorer Development",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspace/MQTT-Explorer",
"customizations": {
"vscode": {
"extensions": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"ms-vscode.vscode-typescript-next",
"ms-azuretools.vscode-docker",
"eamodio.gitlens"
],
"settings": {
"editor.formatOnSave": true,
"typescript.tsdk": "node_modules/typescript/lib",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
}
}
}
},
"forwardPorts": [3000, 8080, 1883, 5900, 6080],
"portsAttributes": {
"3000": {
"label": "MQTT Explorer Server",
"onAutoForward": "notify"
},
"8080": {
"label": "Webpack Dev Server",
"onAutoForward": "notify"
},
"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"
}
+23
View File
@@ -0,0 +1,23 @@
version: '3.8'
services:
app:
image: mcr.microsoft.com/devcontainers/javascript-node:24
volumes:
- ..:/workspaces/MQTT-Explorer:cached
command: sleep infinity
network_mode: service:mosquitto
environment:
- MQTT_EXPLORER_USERNAME=dev
- MQTT_EXPLORER_PASSWORD=dev123
mosquitto:
image: eclipse-mosquitto:2
ports:
- '1883:1883'
- '3000:3000'
- '8080:8080'
- '5900:5900'
- '6080:6080'
volumes:
- ./mosquitto.conf:/mosquitto/config/mosquitto.conf:ro
+4
View File
@@ -0,0 +1,4 @@
# Mosquitto configuration for development
listener 1883
allow_anonymous true
persistence false
+64
View File
@@ -0,0 +1,64 @@
# Git
.git
.gitignore
.github
# Dependencies
node_modules
app/node_modules
backend/node_modules
# Build artifacts
build
dist
app/dist
# Testing
coverage
.nyc_output
test-screenshot-*.png
ui-test.mp4
ui-test.gif
# Development
.vscode
.devcontainer
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# OS
.DS_Store
Thumbs.db
# IDE
*.swp
*.swo
*~
.idea
# Documentation
*.md
!Readme.md
LICENSE.md
# CI/CD files
.releaserc
appveyor.yml
# Misc
res
scripts
docker
icon.xcf
greenkeeper.json
prettier.config.js
.prettierignore
.eslintrc.json
.cspell.json
tslint.json
mcp.json
# Data directory (will be created in container)
data
+17
View File
@@ -0,0 +1,17 @@
{
"env": {
"browser": true,
"commonjs": true,
"es6": true
},
"extends": "eslint:recommended",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018
},
"rules": {
}
}
+3
View File
@@ -0,0 +1,3 @@
package.ts @thomasnordquist
.github @thomasnordquist
scripts @thomasnordquist
+5
View File
@@ -0,0 +1,5 @@
# These are supported funding model platforms
patreon: thomasnordquist
custom: https://paypal.me/ThomasNordquist
+263
View File
@@ -0,0 +1,263 @@
# GitHub Copilot Agent Instructions for MQTT Explorer
## Test Suites
MQTT Explorer has several test suites to ensure code quality and reliability:
### Unit Tests
**App tests** - Frontend component and logic tests:
```bash
yarn test:app
# Or: cd app && yarn test
```
**Backend tests** - Data model and business logic tests:
```bash
yarn test:backend
# Or: cd backend && yarn test
```
**Run all unit tests**:
```bash
yarn test
```
### Integration Tests
**UI test suite** - Independent, deterministic browser tests:
```bash
yarn test:ui
# Requires: yarn build
```
**Demo video generation** - UI test recording with video capture:
```bash
yarn test:demo-video
# Requires: Xvfb, mosquitto broker, tmux, ffmpeg
# For development: Use ./scripts/uiTests.sh for full video recording setup
```
**MCP introspection tests** - Model Context Protocol tests:
```bash
yarn test:mcp
```
**Run all tests** (unit + demo-video):
```bash
yarn test:all
```
### CI/CD Test Execution
In CI environments, tests run in isolated containers with all dependencies pre-installed:
- `test` job: Runs unit tests (app + backend)
- `ui-tests` job: Runs UI test suite with screenshots
- `demo-video` job: Generates demo video with full recording setup
- `test-browser` job: Runs browser mode smoke tests
## Debugging Browser Mode
### Prerequisites
- Node.js 24 or higher
- Yarn package manager
- Running Mosquitto MQTT broker (for testing)
### Development Mode (with Hot Reload)
1. **Set credentials (required):**
```bash
export MQTT_EXPLORER_USERNAME=admin
export MQTT_EXPLORER_PASSWORD=your_password
```
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)
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
# Start server in background
export MQTT_EXPLORER_USERNAME=admin
export MQTT_EXPLORER_PASSWORD=test123
node dist/src/server.js &
# 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
```
### Expected UI Flow
1. **Login Page** (https://github.com/user-attachments/assets/383305e1-2169-433c-a668-5a05da0c343a)
- Enter username and password from environment variables
- Click "LOGIN" button
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
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
### Debugging WebSocket Connection
1. **Check server logs:**
```bash
node dist/src/server.js 2>&1 | tee server.log
```
2. **Check browser WebSocket:**
- DevTools → Network → WS tab
- Look for socket.io connection
- Check Messages tab for authentication handshake
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
```
**Production mode:**
```bash
NODE_ENV=production yarn build:server
NODE_ENV=production node dist/src/server.js
# Minified bundles
# Generic error messages (security)
# HSTS enabled
```
### Build Artifacts
After `yarn build:server`, check:
- `dist/src/server.js` - Compiled server code
- `app/build/*.js` - Webpack bundles
- `app/build/index.html` - Entry point HTML
### Troubleshooting Checklist
- [ ] 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
@@ -0,0 +1,74 @@
name: Auto-approve Copilot Workflow Runs
# This workflow automatically approves workflow runs from GitHub Copilot
# when it pushes to a PR. This is needed because workflows using pull_request_target
# require approval for security reasons, but Copilot is a trusted bot.
on:
pull_request_target:
types: [opened, synchronize, reopened]
jobs:
auto-approve:
runs-on: ubuntu-latest
# Only run if the PR author is GitHub Copilot
if: github.event.pull_request.user.login == 'copilot-app[bot]' || github.event.pull_request.user.login == 'github-actions[bot]'
permissions:
actions: write
contents: read
pull-requests: read
steps:
- name: Get pending workflow runs
id: get-runs
uses: actions/github-script@v7
with:
script: |
const { data: runs } = await github.rest.actions.listWorkflowRunsForRepo({
owner: context.repo.owner,
repo: context.repo.repo,
event: 'pull_request_target',
status: 'waiting',
per_page: 100
});
// Filter runs for this PR
const prNumber = context.payload.pull_request.number;
const prRuns = runs.workflow_runs.filter(run => {
return run.pull_requests && run.pull_requests.some(pr => pr.number === prNumber);
});
console.log(`Found ${prRuns.length} waiting workflow runs for PR #${prNumber}`);
for (const run of prRuns) {
console.log(`- ${run.name} (ID: ${run.id})`);
}
return prRuns.map(run => run.id);
- name: Approve workflow runs
uses: actions/github-script@v7
with:
script: |
const runIds = ${{ steps.get-runs.outputs.result }};
if (!runIds || runIds.length === 0) {
console.log('No workflow runs to approve');
return;
}
for (const runId of runIds) {
try {
await github.rest.actions.approveWorkflowRun({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: runId
});
console.log(`✓ Approved workflow run ${runId}`);
} catch (error) {
console.error(`✗ Failed to approve workflow run ${runId}:`, error.message);
}
}
console.log(`\nApproved ${runIds.length} workflow run(s) for Copilot PR #${context.payload.pull_request.number}`);
+29
View File
@@ -0,0 +1,29 @@
on:
push:
branches:
- master
- release
- beta
paths:
- Dockerfile
- .github
jobs:
create-image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/thomasnordquist/mqtt-explorer-ui-tests:latest
+49
View File
@@ -0,0 +1,49 @@
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_dispatch:
push:
paths:
- .github/workflows/copilot-setup-steps.yml
pull_request:
paths:
- .github/workflows/copilot-setup-steps.yml
jobs:
copilot-setup-steps:
runs-on: ubuntu-latest
steps:
- name: Checkout code
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: '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
with:
path: |
${{ steps.yarn-cache-dir-path.outputs.dir }}
node_modules
app/node_modules
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Install dependencies
run: yarn
+219
View File
@@ -0,0 +1,219 @@
name: Docker Browser Build
on:
push:
branches:
- master
- release
- beta
paths:
- 'Dockerfile.browser'
- 'src/server.ts'
- 'src/AuthManager.ts'
- 'app/**'
- 'backend/**'
- 'package.json'
- 'yarn.lock'
- '.github/workflows/docker-browser.yml'
- 'tsconfig.json'
- 'events/**'
schedule:
# Run every two weeks (1st and 15th of each month) at 2:00 AM UTC
- cron: '0 2 1,15 * *'
workflow_dispatch:
jobs:
build-and-test:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
services:
# MQTT broker for testing
mosquitto:
image: eclipse-mosquitto:2
ports:
- 1883:1883
options: >-
--health-cmd "mosquitto_sub -t '$SYS/#' -C 1"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
tags: |
type=ref,event=branch
type=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}
- name: Build Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile.browser
platforms: linux/amd64
push: false
load: true
tags: mqtt-explorer:test
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Test Docker image - Basic startup
run: |
# Start container with test credentials
docker run -d \
--name mqtt-explorer-test \
-p 3000:3000 \
-e MQTT_EXPLORER_USERNAME=test \
-e MQTT_EXPLORER_PASSWORD=test123 \
-e PORT=3000 \
mqtt-explorer:test
# Wait for server to be ready (max 60 seconds)
echo "Waiting for server to start..."
for i in {1..60}; do
if curl -f http://localhost:3000 > /dev/null 2>&1; then
echo "Server started successfully after $i seconds"
break
fi
if [ $i -eq 60 ]; then
echo "Server failed to start within 60 seconds"
docker logs mqtt-explorer-test
exit 1
fi
sleep 1
done
- name: Test Docker image - Health check
run: |
# Wait for health check to pass
echo "Waiting for health check to pass..."
for i in {1..30}; do
health=$(docker inspect --format='{{.State.Health.Status}}' mqtt-explorer-test)
if [ "$health" = "healthy" ]; then
echo "Container is healthy"
break
fi
if [ $i -eq 30 ]; then
echo "Health check failed"
docker logs mqtt-explorer-test
exit 1
fi
sleep 2
done
- name: Test Docker image - Verify response
run: |
# Test that the server responds with HTML
response=$(curl -s http://localhost:3000)
if echo "$response" | grep -q "MQTT Explorer"; then
echo "Server is serving the application correctly"
else
echo "Server response does not contain expected content"
echo "Response: $response"
exit 1
fi
- name: Test Docker image - Verify data persistence
run: |
# Check that data directory was created
docker exec mqtt-explorer-test sh -c '[ -d /app/data ] && echo "Data directory exists"'
- name: Clean up test container
if: always()
run: |
docker stop mqtt-explorer-test || true
docker rm mqtt-explorer-test || true
- name: Check Docker image size
run: |
echo "### Docker Image Size" >> $GITHUB_STEP_SUMMARY
docker images mqtt-explorer:test --format "table {{.Repository}}\t{{.Tag}}\t{{.Size}}" >> $GITHUB_STEP_SUMMARY
# Get size in bytes for detailed reporting
SIZE_BYTES=$(docker inspect mqtt-explorer:test --format='{{.Size}}')
SIZE_MB=$((SIZE_BYTES / 1024 / 1024))
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Image size**: ${SIZE_MB} MB (${SIZE_BYTES} bytes)" >> $GITHUB_STEP_SUMMARY
echo "Image size: ${SIZE_MB} MB"
- name: Setup Node.js for browser tests
uses: actions/setup-node@v4
with:
node-version: '24'
cache: 'yarn'
- name: Install dependencies for browser tests
run: yarn install --frozen-lockfile
- name: Start Docker container for browser tests
run: |
docker run -d \
--name mqtt-explorer-browser-test \
--network host \
-e MQTT_EXPLORER_USERNAME=test \
-e MQTT_EXPLORER_PASSWORD=test123 \
-e PORT=3000 \
mqtt-explorer:test
# Wait for server to be ready
echo "Waiting for Docker container to be ready..."
timeout 60 bash -c 'until curl -f http://localhost:3000; do sleep 1; done'
echo "Docker container is ready"
- name: Run browser test suite
run: |
yarn test:browser
env:
MQTT_EXPLORER_USERNAME: test
MQTT_EXPLORER_PASSWORD: test123
BROWSER_MODE_URL: http://localhost:3000
MQTT_BROKER_HOST: localhost
MQTT_BROKER_PORT: 1883
- name: Clean up browser test container
if: always()
run: |
docker logs mqtt-explorer-browser-test || true
docker stop mqtt-explorer-browser-test || true
docker rm mqtt-explorer-browser-test || true
- name: Build and push Docker image
id: build
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile.browser
platforms: linux/amd64,linux/arm64,linux/arm/v7
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v1
with:
subject-name: ghcr.io/${{ github.repository }}
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true
+56
View File
@@ -0,0 +1,56 @@
name: Build
on:
push:
branches:
- release
- beta
concurrency:
group: ${{ github.ref }}
cancel-in-progress: false
jobs:
build:
strategy:
matrix:
build:
- os: ubuntu-latest
task: linux
- os: windows-latest
task: win
- os: macos-latest
task: mac
runs-on: ${{ matrix.build.os }}
steps:
- if: matrix.build.os == 'ubuntu-latest'
run: sudo snap install snapcraft --classic
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 24
- run: npm install -g yarn
- run: yarn
- id: create_token # get ReleaseBot access token
uses: actions/create-github-app-token@v1
with:
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
env:
GITHUB_TOKEN: ${{ steps.create_token.outputs.token }}
- run: yarn build
if: steps.semantic.outputs.new_release_published == 'true'
- run: yarn prepare-release
if: steps.semantic.outputs.new_release_published == 'true'
- run: yarn package ${{ matrix.build.task }}
if: steps.semantic.outputs.new_release_published == 'true'
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 }}
+135
View File
@@ -0,0 +1,135 @@
on:
pull_request_target: # Use pull_request_target
branches: [master, beta, release]
jobs:
test:
runs-on: ubuntu-latest
container:
image: ghcr.io/thomasnordquist/mqtt-explorer-ui-tests:latest
volumes:
- ./:/app
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
run: yarn build
- name: Test
run: yarn test
electron-tests:
runs-on: ubuntu-latest
container:
image: ghcr.io/thomasnordquist/mqtt-explorer-ui-tests:latest
volumes:
- ./:/app
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
run: yarn build
- name: Run Electron UI Tests
timeout-minutes: 10
run: ./scripts/runUiTests.sh
- name: Upload Test Screenshots
if: always()
uses: actions/upload-artifact@v4
with:
name: electron-test-screenshots
path: |
test-screenshot-*.png
retention-days: 30
demo-video:
runs-on: ubuntu-latest
container:
image: ghcr.io/thomasnordquist/mqtt-explorer-ui-tests:latest
volumes:
- ./:/app
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
run: yarn build
- name: Generate Demo Video
run: yarn ui-test
- name: Post-processing
run: ./scripts/prepareVideo.sh
- uses: hkusu/s3-upload-action@v2
id: upload # specify some ID for use in subsequent steps
with:
aws-access-key-id: ${{ vars.AWS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: 'eu-central-1'
aws-bucket: ${{ vars.AWS_BUCKET }}
file-path: './ui-test.gif'
content-type: image/gif
output-file-url: 'true'
- name: Show URL
run: echo '${{ steps.upload.outputs.file-url }}'
id: artifact-upload-step
- run: echo '<picture><img src="${{ steps.upload.outputs.file-url }}"></picture>' >> $GITHUB_STEP_SUMMARY
test-browser:
runs-on: ubuntu-latest
services:
mosquitto:
image: eclipse-mosquitto:2
ports:
- 1883:1883
options: >-
--health-cmd "mosquitto_sub -t '$SYS/#' -C 1"
--health-interval 10s
--health-timeout 5s
--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: '24'
- name: Install Dependencies
run: yarn install --frozen-lockfile
- name: Build Browser Mode
run: yarn build:server
- name: Test App
run: yarn test:app
- name: Test Backend
run: yarn test:backend
- name: Start Server in Background
run: |
yarn start:server &
echo $! > server.pid
env:
MQTT_EXPLORER_USERNAME: test
MQTT_EXPLORER_PASSWORD: test123
PORT: 3000
- name: Wait for Server
run: |
timeout 30 bash -c 'until curl -f http://localhost:3000; do sleep 1; done'
- name: Browser Smoke Test
run: |
# Test server is running
curl -f http://localhost:3000 || exit 1
echo "Browser mode server is running successfully"
- name: Stop Server
if: always()
run: |
if [ -f server.pid ]; then
kill $(cat server.pid) || true
rm server.pid
fi
+17
View File
@@ -0,0 +1,17 @@
name: Update Website
on: [release, workflow_dispatch]
jobs:
update-website:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: gh-pages
- uses: actions/setup-node@v4
with:
node-version: '24'
- run: npm install
- run: npm run readme
- uses: stefanzweifel/git-auto-commit-action@v5
+13
View File
@@ -7,3 +7,16 @@ build
test.dot
test.png
.awcache
.scannerwork
screen*.png
# MCP introspection artifacts
mqtt-explorer-mcp-screenshot.png
screenshot-mcp-*.png
test-mcp-introspection.js
/data
test-screenshot-*.png
test-expand-*.png
app/.webpack-cache
+2
View File
@@ -0,0 +1,2 @@
node_modules
build
+30
View File
@@ -0,0 +1,30 @@
{
"branches": [
"release",
{
"name": "beta",
"prerelease": true
}
],
repositoryUrl: "git@github.com:thomasnordquist/MQTT-Explorer.git",
"plugins": [
"@semantic-release/commit-analyzer",
"semantic-release-export-data",
"@semantic-release/changelog",
[
"@semantic-release/npm",
{
"npmPublish": false
}
],
[
"@semantic-release/git",
{
"assets": [
"package.json",
"yarn.lock"
]
}
]
]
}
-36
View File
@@ -1,36 +0,0 @@
language: node_js
services:
- xvfb
cache:
directories:
- node_modules
- $HOME/.cache/electron
- $HOME/.cache/electron-builder
- $HOME/.npm/_prebuilds
node_js:
- "10"
os:
- linux
- osx
dist: xenial
services:
- docker
install:
- yarn install
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then docker build docker --tag uitest; fi;
script:
- yarn run build
- yarn test
- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then docker run -e GH_TOKEN=$GH_TOKEN -e GIT_TAG=$TRAVIS_TAG --rm -v `pwd`:/app uitest sh -c "cd app && docker/testMounted.sh"; fi
- if [[ "$TRAVIS_TAG" != "" ]]; then yarn run prepare-release; fi
- if [[ "$TRAVIS_OS_NAME" == "linux" ]] && [[ "$TRAVIS_TAG" != "" ]]; then yarn run package -- linux; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]] && [[ "$TRAVIS_TAG" != "" ]]; then yarn run package -- mac; fi
- if [[ "$TRAVIS_OS_NAME" == "osx" ]] && [[ "$TRAVIS_TAG" != "" ]]; then yarn run package -- win; fi
+7
View File
@@ -0,0 +1,7 @@
{
"editor.formatOnSave": true,
"files.exclude": {
"**/node_modules": true,
"build/": true
}
}
+307
View File
@@ -0,0 +1,307 @@
# Browser Mode Documentation
MQTT Explorer now supports running as a web application served by a Node.js server, in addition to the existing Electron desktop app.
## Running in Browser Mode
### Quick Start
1. Build the application for browser mode:
```bash
yarn build:server
```
2. Start the server:
```bash
yarn start:server
```
3. Open your browser and navigate to `http://localhost:3000`
4. You'll be prompted to log in with credentials that were generated on server startup.
### Development Mode
To run in development mode with hot reload:
```bash
yarn dev:server
```
This starts both the webpack dev server and the backend server.
## Authentication
### Environment Variables
You can set custom authentication credentials using environment variables:
```bash
export MQTT_EXPLORER_USERNAME=admin
export MQTT_EXPLORER_PASSWORD=secretpassword
yarn start:server
```
### Generated Credentials
If no environment variables are set, the server will generate credentials on first startup and save them to `data/credentials.json`. The generated credentials will be printed to the console:
```
============================================================
Generated new credentials:
Username: user-abc123
Password: 123e4567-e89b-12d3-a456-426614174000
============================================================
Please save these credentials. They will be persisted to:
/path/to/data/credentials.json
============================================================
```
## Features
### Certificate Upload
In browser mode, certificate files are uploaded directly through the browser using the HTML5 File API. The certificates are:
- Read client-side as base64
- Stored in the connection configuration
- Used when establishing MQTT connections
### Data Storage
In browser mode, all data is stored on the server:
- Credentials: `data/credentials.json`
- Uploaded certificates: `data/certificates/`
- File uploads: `data/uploads/`
### Port Configuration
The default port is 3000. You can change it using the `PORT` environment variable:
```bash
PORT=8080 yarn start:server
```
## Architecture
### Client-Server Communication
- **Electron Mode**: Uses Electron IPC for communication between renderer and main process
- **Browser Mode**: Uses Socket.io WebSockets for real-time communication between browser and server
The application automatically detects the environment and uses the appropriate transport layer.
### Event Bus Abstraction
Both Electron IPC and Socket.io implement the same `EventBusInterface`, allowing the application code to work seamlessly in both modes without modification.
## Differences from Electron Mode
### Browser Mode Limitations
1. **File System Access**: Limited to server-side operations
2. **Native Dialogs**: File selection uses browser file input instead of native dialogs
3. **Auto-Updates**: Not available in browser mode
4. **Tray Icon**: Not available in browser mode
### Browser Mode Advantages
1. **No Installation**: Access from any browser
2. **Cross-Platform**: Works on any device with a modern browser
3. **Remote Access**: Can be deployed on a server for remote access
4. **Multi-User**: Can support authentication for multiple users
## 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
4. **Environment Variables**: Use environment variables for production credentials, not the generated ones
## Deployment
For production deployment:
1. Build the application:
```bash
yarn build:server
```
2. Set environment variables:
```bash
export MQTT_EXPLORER_USERNAME=your_username
export MQTT_EXPLORER_PASSWORD=your_secure_password
export PORT=3000
```
3. Start the server:
```bash
yarn start:server
```
4. Use a reverse proxy (nginx, Apache) to add HTTPS and additional security features
## Troubleshooting
### Debugging
Enable detailed Socket.IO connection and lifecycle debugging:
```bash
DEBUG=mqtt-explorer:socketio* yarn start:server
```
Available debug namespaces:
- `mqtt-explorer:socketio` - General Socket.IO events and metrics
- `mqtt-explorer:socketio:connect` - Client connection events
- `mqtt-explorer:socketio:disconnect` - Client disconnection and cleanup
- `mqtt-explorer:socketio:subscriptions` - Subscription lifecycle tracking
- `mqtt-explorer:socketio:connections` - MQTT connection ownership
This will log:
- Client connect/disconnect events
- Subscription counts per socket
- MQTT connection ownership tracking
- Memory leak detection metrics (subscriptions, handlers, connections)
Example output:
```
mqtt-explorer:socketio:connect Client connected: abc123de
mqtt-explorer:socketio [connect] clients=1 subscriptions=8 mqttConns=0 | socket[abc123de]: subs=8 conns=0
mqtt-explorer:socketio:connections Connection my-mqtt owned by socket abc123de (total: 1)
mqtt-explorer:socketio:disconnect Client disconnected: abc123de
mqtt-explorer:socketio:subscriptions Removed 8 subscriptions for socket abc123de
mqtt-explorer:socketio [disconnect] clients=0 subscriptions=0 mqttConns=0 | socket[abc123de]: subs=0 conns=0
```
### Authentication Fails
1. Check the console output for the generated credentials
2. Clear browser session storage: `sessionStorage.clear()` in browser console
3. Restart the server to regenerate credentials
### Connection Issues
1. Check that the server is running: `http://localhost:3000`
2. Check browser console for Socket.io connection errors
3. Verify firewall rules allow the port
### Certificate Upload Issues
In browser mode, certificates are handled differently:
- Use the file upload button to select certificate files
- Files are read and encoded client-side
- Large certificate files (>16KB) will be rejected
+226
View File
@@ -0,0 +1,226 @@
# CI/CD Pipeline Documentation
## Overview
MQTT Explorer uses GitHub Actions for continuous integration and testing. The pipeline tests both Electron (desktop) and browser modes.
## Workflows
### Test Workflow (`.github/workflows/tests.yml`)
This workflow runs on pull requests to `master`, `beta`, and `release` branches.
### Docker Browser Build Workflow (`.github/workflows/docker-browser.yml`)
This workflow builds and publishes a Docker image for the browser mode.
**Triggers**:
- Push to `master`, `beta`, or `release` branches (when relevant files change)
- Schedule: Runs every two weeks (1st and 15th of each month at 2:00 AM UTC)
- Manual trigger via workflow_dispatch
**Platforms**:
- linux/amd64 (x86-64)
- linux/arm64 (Raspberry Pi 3/4/5, Apple Silicon)
- linux/arm/v7 (Raspberry Pi 2/3)
**Image Registry**: GitHub Container Registry (ghcr.io/thomasnordquist/mqtt-explorer)
**Tags**:
- `latest` - Latest build from master branch
- `master` - Latest build from master
- `beta` - Latest build from beta branch
- `release` - Latest build from release branch
- `<branch>-<sha>` - Specific commit builds
**Steps**:
1. Build Docker image with multi-stage build
2. Test basic startup with test credentials
3. Test health check
4. Verify HTTP response
5. Test data directory creation
6. Check Docker image size
7. Start container for frontend tests
8. Test frontend bundles (app.bundle.js, vendors.bundle.js)
9. Push image to GitHub Container Registry
10. Generate build attestation for supply chain security
**Image Features**:
- Multi-stage build for minimal size
- Alpine Linux base with Node.js 24 (~200MB final image)
- Multi-platform support (amd64, arm64, arm/v7)
- Non-root user (UID 1001)
- Health check endpoint
- Proper signal handling with dumb-init
- Persistent data volume at `/app/data`
### Test Workflow (`.github/workflows/tests.yml`)
This workflow runs on pull requests to `master`, `beta`, and `release` branches.
#### Jobs
##### 1. `test` - Electron Mode Tests
Tests the traditional Electron desktop application:
- **Environment**: Custom Docker container (`ghcr.io/thomasnordquist/mqtt-explorer-ui-tests:latest`)
- **Steps**:
1. Install dependencies with frozen lockfile
2. Build the Electron application
3. Run unit tests (app + backend)
4. Run UI tests with video recording
5. Upload test video to S3
6. Display test results in GitHub summary
**Artifacts**: UI test video (GIF format) uploaded to S3
##### 2. `test-browser` - Browser Mode Tests
Tests the new browser/server mode:
- **Environment**: Ubuntu latest with Node.js 20
- **Services**:
- **Mosquitto MQTT Broker**: Eclipse Mosquitto v2 on port 1883
- Health checks enabled
- Anonymous connections allowed
- **Steps**:
1. Setup Node.js 20
2. Install dependencies
3. Build browser mode (`yarn build:server`)
4. Run unit tests (app + backend)
5. Start server in background with test credentials
6. Wait for server to be ready
7. Run browser smoke tests
8. Clean up server process
**Environment Variables**:
- `MQTT_EXPLORER_USERNAME=test`
- `MQTT_EXPLORER_PASSWORD=test123`
- `PORT=3000`
## Test Commands
The following npm scripts are used in CI/CD:
```bash
# Unit tests
yarn test # Run all tests (app + backend)
yarn test:app # Frontend tests only
yarn test:backend # Backend tests only
# Build
yarn build # Build Electron mode
yarn build:server # Build browser mode
# UI Tests (Electron only)
yarn ui-test # Run UI tests with video recording
```
## Adding New Tests
### For Electron Mode
Add tests to the `test` job. UI tests should be added to the test suite that `yarn ui-test` runs.
### For Browser Mode
Browser-specific tests should:
1. Use the pre-configured Mosquitto service
2. Connect to `mqtt://mosquitto:1883`
3. Test server endpoints at `http://localhost:3000`
Example:
```yaml
- name: Browser Integration Test
run: |
# Test MQTT connection through server
curl -X POST http://localhost:3000/api/test
```
## Local Testing
### Docker Browser Mode
```bash
# Build the image locally (for your platform)
docker build -f Dockerfile.browser -t mqtt-explorer:local .
# Build for specific platform (e.g., Raspberry Pi)
docker buildx build --platform linux/arm64 -f Dockerfile.browser -t mqtt-explorer:local-arm64 .
# Run the container
docker run -d \
-p 3000:3000 \
-e MQTT_EXPLORER_USERNAME=test \
-e MQTT_EXPLORER_PASSWORD=test123 \
mqtt-explorer:local
# Test the server
curl http://localhost:3000
# Check logs
docker logs <container-id>
# Stop and remove
docker stop <container-id>
docker rm <container-id>
```
See [DOCKER.md](DOCKER.md) for complete documentation.
### Electron Mode
```bash
yarn build
yarn test
yarn ui-test
```
### Browser Mode
```bash
# Start Mosquitto in Docker
docker run -d -p 1883:1883 eclipse-mosquitto:2
# Build and test
yarn build:server
yarn test
# Start server
MQTT_EXPLORER_USERNAME=test MQTT_EXPLORER_PASSWORD=test123 yarn start:server
# Run manual tests
curl http://localhost:3000
```
## GitHub Codespaces / Devcontainer
The repository includes a devcontainer configuration that automatically sets up:
- Node.js 20
- MQTT broker (Mosquitto)
- All development dependencies
- Port forwarding for development
See [.devcontainer/README.md](.devcontainer/README.md) for details.
## Troubleshooting
### Browser Tests Failing
1. **Server won't start**: Check if port 3000 is already in use
2. **MQTT connection fails**: Ensure Mosquitto service is healthy
3. **Timeout errors**: Increase timeout in "Wait for Server" step
### Electron Tests Failing
1. **UI tests timeout**: Check if the Docker container has display access
2. **Build fails**: Verify all dependencies are in yarn.lock
## Future Improvements
- [ ] Add E2E browser tests with Playwright
- [ ] Test WebSocket connections in browser mode
- [ ] Add performance benchmarks
- [ ] Test with different MQTT broker versions
- [ ] Add security scanning for browser mode
+12
View File
@@ -0,0 +1,12 @@
# 0.2.3
- Highlight differences in the last received message with a "diff" view
- Magnify app content with "Ctrl +", "Ctrl -", "Ctrl 0"
- Make user interactions more predictable by removing "select topic on mouse over"
- Add "Quick Preview" setting, to enable topic selection on mouse over
- Add JSON formatter
- Show error when connection to the mqtt broker is lost
- Fix bug where mqtt explorer resets the tree after a reconnect
- Fix MQTT-Client id
# 0.2.0
+250
View File
@@ -0,0 +1,250 @@
# MQTT Explorer - Docker Browser Mode
Docker image for running MQTT Explorer in browser mode.
## Try It Now
[![Try in PWD](https://raw.githubusercontent.com/play-with-docker/stacks/master/assets/images/button.png)](https://labs.play-with-docker.com/?stack=https://raw.githubusercontent.com/thomasnordquist/MQTT-Explorer/master/docker-compose.yml)
Click the badge above to instantly try MQTT Explorer in your browser using Play with Docker (requires free Docker Hub account).
## Quick Start
### Using Pre-built Image
Pull and run the latest image from GitHub Container Registry:
```bash
docker pull ghcr.io/thomasnordquist/mqtt-explorer:latest
docker run -d \
-p 3000:3000 \
-e MQTT_EXPLORER_USERNAME=admin \
-e MQTT_EXPLORER_PASSWORD=your_secure_password \
-v mqtt-explorer-data:/app/data \
--name mqtt-explorer \
ghcr.io/thomasnordquist/mqtt-explorer:latest
```
Access the application at `http://localhost:3000`
### Using Docker Compose
Create a `docker-compose.yml` file:
```yaml
version: '3.8'
services:
mqtt-explorer:
image: ghcr.io/thomasnordquist/mqtt-explorer:latest
ports:
- "3000:3000"
environment:
- MQTT_EXPLORER_USERNAME=admin
- MQTT_EXPLORER_PASSWORD=your_secure_password
- PORT=3000
volumes:
- mqtt-explorer-data:/app/data
restart: unless-stopped
volumes:
mqtt-explorer-data:
```
Then run:
```bash
docker-compose up -d
```
## Environment Variables
| Variable | Required | Default | Description |
|----------|----------|---------|-------------|
| `MQTT_EXPLORER_USERNAME` | No | Generated | Username for authentication |
| `MQTT_EXPLORER_PASSWORD` | No | Generated | Password for authentication |
| `MQTT_EXPLORER_SKIP_AUTH` | No | `false` | Set to `true` to disable authentication (use only behind a secure proxy!) |
| `PORT` | No | `3000` | Port the server listens on |
| `ALLOWED_ORIGINS` | No | `*` | Comma-separated list of allowed CORS origins |
| `NODE_ENV` | No | - | Set to `production` for production deployments |
### Authentication Modes
**Standard Mode (Default):**
- Requires username and password for access
- Credentials can be set via environment variables or auto-generated
- Auto-generated credentials are logged on first startup and saved to `/app/data/credentials.json`
**Skip Authentication Mode (Use with caution!):**
```bash
docker run -d -p 3000:3000 \
-e MQTT_EXPLORER_SKIP_AUTH=true \
ghcr.io/thomasnordquist/mqtt-explorer:latest
```
⚠️ **WARNING**: When `MQTT_EXPLORER_SKIP_AUTH=true`, the application is **completely open** without any authentication. This should **only be used** when MQTT Explorer is deployed behind a secure authentication proxy (e.g., OAuth2 Proxy, Authelia, Nginx with auth_request) or in a trusted private network.
**Recommended use case**: Integration with enterprise SSO systems where authentication is handled by a reverse proxy.
**Note**: If credentials are not provided and auth is not skipped, they will be auto-generated and stored in `/app/data/credentials.json`. Check the container logs to see the generated credentials:
```bash
docker logs mqtt-explorer
```
## Data Persistence
The container stores data in `/app/data`, including:
- User credentials (`credentials.json`)
- Connection settings (`settings.json`)
- Uploaded certificates (`certificates/`)
- File uploads (`uploads/`)
Mount a volume to persist data across container restarts:
```bash
docker run -v mqtt-explorer-data:/app/data ...
```
## Building from Source
Build the Docker image locally:
```bash
docker build -f Dockerfile.browser -t mqtt-explorer:local .
```
Run the locally built image:
```bash
docker run -d \
-p 3000:3000 \
-e MQTT_EXPLORER_USERNAME=admin \
-e MQTT_EXPLORER_PASSWORD=secret \
mqtt-explorer:local
```
## Health Check
The container includes a health check that runs every 30 seconds. Check the health status:
```bash
docker inspect --format='{{.State.Health.Status}}' mqtt-explorer
```
## Security Best Practices
1. **Use HTTPS in Production**: Put the container behind a reverse proxy (nginx, Traefik) with HTTPS
2. **Set Strong Credentials**: Always set custom credentials via environment variables
3. **Network Isolation**: Run in a private network when possible
4. **Update Regularly**: Pull the latest image regularly for security updates
### Example with Nginx Reverse Proxy
```nginx
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;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
## Troubleshooting
### Container won't start
Check the logs:
```bash
docker logs mqtt-explorer
```
### Can't access the application
1. Verify the container is running: `docker ps`
2. Check the port mapping: `docker port mqtt-explorer`
3. Test connectivity: `curl http://localhost:3000`
### Authentication issues
1. Check generated credentials in logs: `docker logs mqtt-explorer`
2. Verify environment variables: `docker inspect mqtt-explorer`
3. Reset credentials by removing the data volume and restarting
### Permission issues
The container runs as a non-root user (UID 1001). If mounting host directories, ensure they're writable:
```bash
chown -R 1001:1001 /path/to/host/data
docker run -v /path/to/host/data:/app/data ...
```
## Available Tags
- `latest` - Latest stable version from the master branch
- `master` - Latest build from master branch
- `beta` - Latest beta version
- `release` - Latest release version
- `master-<sha>` - Specific commit from master
- `beta-<sha>` - Specific commit from beta
- `release-<sha>` - Specific commit from release
## Supported Platforms
The Docker image is built for multiple architectures:
- `linux/amd64` - x86-64 (standard PCs, servers)
- `linux/arm64` - ARM 64-bit (Raspberry Pi 3/4/5, Apple Silicon)
- `linux/arm/v7` - ARM 32-bit (Raspberry Pi 2/3)
## One-Click Deployment Options
### Play with Docker (Free)
Try MQTT Explorer instantly in your browser without installing anything:
[![Try in PWD](https://raw.githubusercontent.com/play-with-docker/stacks/master/assets/images/button.png)](https://labs.play-with-docker.com/?stack=https://raw.githubusercontent.com/thomasnordquist/MQTT-Explorer/master/docker-compose.yml)
- **No installation required** - Runs entirely in your browser
- **Free to use** - Requires only a Docker Hub account
- **Perfect for demos** - Great for testing and demonstrations
- **4-hour sessions** - Sessions automatically expire after 4 hours
### Cloud Platforms
Deploy MQTT Explorer to various cloud platforms with one click:
#### DigitalOcean App Platform
[![Deploy to DO](https://www.deploytodo.com/do-btn-blue.svg)](https://cloud.digitalocean.com/apps/new?repo=https://github.com/thomasnordquist/MQTT-Explorer/tree/master&refcode=docker)
- Automatically detects Docker configuration
- Managed platform with auto-scaling
- Starting at $5/month
#### Koyeb
[![Deploy to Koyeb](https://www.koyeb.com/static/images/deploy/button.svg)](https://app.koyeb.com/deploy?type=docker&name=mqtt-explorer&image=ghcr.io/thomasnordquist/mqtt-explorer:latest&ports=3000;http;/)
- Deploy directly from Docker image
- Global edge network
- Free tier available
**Note:** Remember to set the environment variables `MQTT_EXPLORER_USERNAME` and `MQTT_EXPLORER_PASSWORD` when deploying to cloud platforms.
## License
See the main [LICENSE.md](LICENSE.md) file.
+2 -3
View File
@@ -1,7 +1,7 @@
FROM node:11-stretch
FROM node:24
RUN DEBIAN_FRONTEND="noninteractive" apt-get update \
&& apt-get install -y --no-install-recommends nano ffmpeg xvfb git-core tmux locales mosquitto x11vnc
&& apt-get install -y --no-install-recommends ca-certificates nano ffmpeg xvfb git-core tmux locales mosquitto x11vnc
RUN apt-get install -yq --no-install-recommends libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 libnss3
# Generate locales for TMUX
@@ -12,6 +12,5 @@ ENV LC_ALL en_US.UTF-8
CMD /bin/bash
COPY cloneBuildAndTest.sh ./
VOLUME /app
EXPOSE 5900
+81
View File
@@ -0,0 +1,81 @@
# Multi-stage build for MQTT Explorer Browser Mode
# Stage 1: Build
FROM node:24-alpine AS builder
WORKDIR /build
# Copy package files for dependency installation
COPY package.json yarn.lock ./
COPY app/package.json ./app/
COPY backend/package.json ./backend/
# Install ALL dependencies (needed for build)
RUN yarn install --frozen-lockfile --network-timeout 100000
# Copy source files
COPY tsconfig.json ./
COPY src ./src
COPY backend ./backend
COPY events ./events
COPY app ./app
# Build the application (compiles TypeScript and webpack bundles)
RUN yarn build:server
# Stage 2: Production dependencies
FROM node:24-alpine AS deps
WORKDIR /deps
# Copy only package files
COPY --from=builder /build/package.json /build/yarn.lock ./
# Install ONLY production dependencies
RUN yarn install --production --frozen-lockfile --network-timeout 100000 && \
yarn cache clean && \
rm -rf /tmp/*
# Stage 3: Production
FROM node:24-alpine
# Install dumb-init in a single layer
RUN apk add --no-cache dumb-init
# Create app user in a single layer
RUN addgroup -g 1001 -S mqttexplorer && \
adduser -u 1001 -S mqttexplorer -G mqttexplorer
WORKDIR /app
# Copy ONLY the compiled dist folder (contains compiled TypeScript)
COPY --from=builder --chown=mqttexplorer:mqttexplorer /build/dist ./dist
# Copy ONLY the built frontend app
COPY --from=builder --chown=mqttexplorer:mqttexplorer /build/app/build ./app/build
COPY --from=builder --chown=mqttexplorer:mqttexplorer /build/app/index.html ./app/
# Copy runtime node_modules (minimal set)
COPY --from=deps --chown=mqttexplorer:mqttexplorer /deps/node_modules ./node_modules
# Copy package.json for version info (needed by server)
COPY --from=builder --chown=mqttexplorer:mqttexplorer /build/package.json ./
# Create data directory for persistent storage
RUN mkdir -p /app/data && \
chown -R mqttexplorer:mqttexplorer /app/data
# Switch to non-root user
USER mqttexplorer
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD node -e "const http = require('http'); const req = http.get('http://localhost:3000', (r) => process.exit(r.statusCode === 200 ? 0 : 1)); req.on('error', () => process.exit(1));"
# Use dumb-init to handle signals properly
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
# Start the server
CMD ["node", "dist/src/server.js"]
+173
View File
@@ -0,0 +1,173 @@
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
Creative Commons Corporation (“Creative Commons”) is not a law firm and does not provide legal services or legal advice. Distribution of Creative Commons public licenses does not create a lawyer-client or other relationship. Creative Commons makes its licenses and related information available on an “as-is” basis. Creative Commons gives no warranties regarding its licenses, any material licensed under their terms and conditions, or any related information. Creative Commons disclaims all liability for damages resulting from their use to the fullest extent possible.
**Using Creative Commons Public Licenses**
Creative Commons public licenses provide a standard set of terms and conditions that creators and other rights holders may use to share original works of authorship and other material subject to copyright and certain other rights specified in the public license below. The following considerations are for informational purposes only, are not exhaustive, and do not form part of our licenses.
* __Considerations for licensors:__ Our public licenses are intended for use by those authorized to give the public permission to use material in ways otherwise restricted by copyright and certain other rights. Our licenses are irrevocable. Licensors should read and understand the terms and conditions of the license they choose before applying it. Licensors should also secure all rights necessary before applying our licenses so that the public can reuse the material as expected. Licensors should clearly mark any material not subject to the license. This includes other CC-licensed material, or material used under an exception or limitation to copyright. [More considerations for licensors](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensors).
* __Considerations for the public:__ By using one of our public licenses, a licensor grants the public permission to use the licensed material under specified terms and conditions. If the licensors permission is not necessary for any reasonfor example, because of any applicable exception or limitation to copyrightthen that use is not regulated by the license. Our licenses grant only permissions under copyright and certain other rights that a licensor has authority to grant. Use of the licensed material may still be restricted for other reasons, including because others have copyright or other rights in the material. A licensor may make special requests, such as asking that all changes be marked or described. Although not required by our licenses, you are encouraged to respect those requests where reasonable. [More considerations for the public](http://wiki.creativecommons.org/Considerations_for_licensors_and_licensees#Considerations_for_licensees).
## Creative Commons Attribution-ShareAlike 4.0 International Public License
By exercising the Licensed Rights (defined below), You accept and agree to be bound by the terms and conditions of this Creative Commons Attribution-ShareAlike 4.0 International Public License ("Public License"). To the extent this Public License may be interpreted as a contract, You are granted the Licensed Rights in consideration of Your acceptance of these terms and conditions, and the Licensor grants You such rights in consideration of benefits the Licensor receives from making the Licensed Material available under these terms and conditions.
### Section 1 Definitions.
a. __Adapted Material__ means material subject to Copyright and Similar Rights that is derived from or based upon the Licensed Material and in which the Licensed Material is translated, altered, arranged, transformed, or otherwise modified in a manner requiring permission under the Copyright and Similar Rights held by the Licensor. For purposes of this Public License, where the Licensed Material is a musical work, performance, or sound recording, Adapted Material is always produced where the Licensed Material is synched in timed relation with a moving image.
b. __Adapter's License__ means the license You apply to Your Copyright and Similar Rights in Your contributions to Adapted Material in accordance with the terms and conditions of this Public License.
c. __BY-SA Compatible License__ means a license listed at [creativecommons.org/compatiblelicenses](http://creativecommons.org/compatiblelicenses), approved by Creative Commons as essentially the equivalent of this Public License.
d. __Copyright and Similar Rights__ means copyright and/or similar rights closely related to copyright including, without limitation, performance, broadcast, sound recording, and Sui Generis Database Rights, without regard to how the rights are labeled or categorized. For purposes of this Public License, the rights specified in Section 2(b)(1)-(2) are not Copyright and Similar Rights.
e. __Effective Technological Measures__ means those measures that, in the absence of proper authority, may not be circumvented under laws fulfilling obligations under Article 11 of the WIPO Copyright Treaty adopted on December 20, 1996, and/or similar international agreements.
f. __Exceptions and Limitations__ means fair use, fair dealing, and/or any other exception or limitation to Copyright and Similar Rights that applies to Your use of the Licensed Material.
g. __License Elements__ means the license attributes listed in the name of a Creative Commons Public License. The License Elements of this Public License are Attribution and ShareAlike.
h. __Licensed Material__ means the artistic or literary work, database, or other material to which the Licensor applied this Public License.
i. __Licensed Rights__ means the rights granted to You subject to the terms and conditions of this Public License, which are limited to all Copyright and Similar Rights that apply to Your use of the Licensed Material and that the Licensor has authority to license.
j. __Licensor__ means the individual(s) or entity(ies) granting rights under this Public License.
k. __Share__ means to provide material to the public by any means or process that requires permission under the Licensed Rights, such as reproduction, public display, public performance, distribution, dissemination, communication, or importation, and to make material available to the public including in ways that members of the public may access the material from a place and at a time individually chosen by them.
l. __Sui Generis Database Rights__ means rights other than copyright resulting from Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, as amended and/or succeeded, as well as other essentially equivalent rights anywhere in the world.
m. __You__ means the individual or entity exercising the Licensed Rights under this Public License. __Your__ has a corresponding meaning.
### Section 2 Scope.
a. ___License grant.___
1. Subject to the terms and conditions of this Public License, the Licensor hereby grants You a worldwide, royalty-free, non-sublicensable, non-exclusive, irrevocable license to exercise the Licensed Rights in the Licensed Material to:
A. reproduce and Share the Licensed Material, in whole or in part; and
B. produce, reproduce, and Share Adapted Material.
2. __Exceptions and Limitations.__ For the avoidance of doubt, where Exceptions and Limitations apply to Your use, this Public License does not apply, and You do not need to comply with its terms and conditions.
3. __Term.__ The term of this Public License is specified in Section 6(a).
4. __Media and formats; technical modifications allowed.__ The Licensor authorizes You to exercise the Licensed Rights in all media and formats whether now known or hereafter created, and to make technical modifications necessary to do so. The Licensor waives and/or agrees not to assert any right or authority to forbid You from making technical modifications necessary to exercise the Licensed Rights, including technical modifications necessary to circumvent Effective Technological Measures. For purposes of this Public License, simply making modifications authorized by this Section 2(a)(4) never produces Adapted Material.
5. __Downstream recipients.__
A. __Offer from the Licensor Licensed Material.__ Every recipient of the Licensed Material automatically receives an offer from the Licensor to exercise the Licensed Rights under the terms and conditions of this Public License.
B. __Additional offer from the Licensor Adapted Material.__ Every recipient of Adapted Material from You automatically receives an offer from the Licensor to exercise the Licensed Rights in the Adapted Material under the conditions of the Adapters License You apply.
C. __No downstream restrictions.__ You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, the Licensed Material if doing so restricts exercise of the Licensed Rights by any recipient of the Licensed Material.
6. __No endorsement.__ Nothing in this Public License constitutes or may be construed as permission to assert or imply that You are, or that Your use of the Licensed Material is, connected with, or sponsored, endorsed, or granted official status by, the Licensor or others designated to receive attribution as provided in Section 3(a)(1)(A)(i).
b. ___Other rights.___
1. Moral rights, such as the right of integrity, are not licensed under this Public License, nor are publicity, privacy, and/or other similar personality rights; however, to the extent possible, the Licensor waives and/or agrees not to assert any such rights held by the Licensor to the limited extent necessary to allow You to exercise the Licensed Rights, but not otherwise.
2. Patent and trademark rights are not licensed under this Public License.
3. To the extent possible, the Licensor waives any right to collect royalties from You for the exercise of the Licensed Rights, whether directly or through a collecting society under any voluntary or waivable statutory or compulsory licensing scheme. In all other cases the Licensor expressly reserves any right to collect such royalties.
### Section 3 License Conditions.
Your exercise of the Licensed Rights is expressly made subject to the following conditions.
a. ___Attribution.___
1. If You Share the Licensed Material (including in modified form), You must:
A. retain the following if it is supplied by the Licensor with the Licensed Material:
i. identification of the creator(s) of the Licensed Material and any others designated to receive attribution, in any reasonable manner requested by the Licensor (including by pseudonym if designated);
ii. a copyright notice;
iii. a notice that refers to this Public License;
iv. a notice that refers to the disclaimer of warranties;
v. a URI or hyperlink to the Licensed Material to the extent reasonably practicable;
B. indicate if You modified the Licensed Material and retain an indication of any previous modifications; and
C. indicate the Licensed Material is licensed under this Public License, and include the text of, or the URI or hyperlink to, this Public License.
2. You may satisfy the conditions in Section 3(a)(1) in any reasonable manner based on the medium, means, and context in which You Share the Licensed Material. For example, it may be reasonable to satisfy the conditions by providing a URI or hyperlink to a resource that includes the required information.
3. If requested by the Licensor, You must remove any of the information required by Section 3(a)(1)(A) to the extent reasonably practicable.
b. ___ShareAlike.___
In addition to the conditions in Section 3(a), if You Share Adapted Material You produce, the following conditions also apply.
1. The Adapters License You apply must be a Creative Commons license with the same License Elements, this version or later, or a BY-SA Compatible License.
2. You must include the text of, or the URI or hyperlink to, the Adapter's License You apply. You may satisfy this condition in any reasonable manner based on the medium, means, and context in which You Share Adapted Material.
3. You may not offer or impose any additional or different terms or conditions on, or apply any Effective Technological Measures to, Adapted Material that restrict exercise of the rights granted under the Adapter's License You apply.
### Section 4 Sui Generis Database Rights.
Where the Licensed Rights include Sui Generis Database Rights that apply to Your use of the Licensed Material:
a. for the avoidance of doubt, Section 2(a)(1) grants You the right to extract, reuse, reproduce, and Share all or a substantial portion of the contents of the database;
b. if You include all or a substantial portion of the database contents in a database in which You have Sui Generis Database Rights, then the database in which You have Sui Generis Database Rights (but not its individual contents) is Adapted Material, including for purposes of Section 3(b); and
c. You must comply with the conditions in Section 3(a) if You Share all or a substantial portion of the contents of the database.
For the avoidance of doubt, this Section 4 supplements and does not replace Your obligations under this Public License where the Licensed Rights include other Copyright and Similar Rights.
### Section 5 Disclaimer of Warranties and Limitation of Liability.
a. __Unless otherwise separately undertaken by the Licensor, to the extent possible, the Licensor offers the Licensed Material as-is and as-available, and makes no representations or warranties of any kind concerning the Licensed Material, whether express, implied, statutory, or other. This includes, without limitation, warranties of title, merchantability, fitness for a particular purpose, non-infringement, absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not known or discoverable. Where disclaimers of warranties are not allowed in full or in part, this disclaimer may not apply to You.__
b. __To the extent possible, in no event will the Licensor be liable to You on any legal theory (including, without limitation, negligence) or otherwise for any direct, special, indirect, incidental, consequential, punitive, exemplary, or other losses, costs, expenses, or damages arising out of this Public License or use of the Licensed Material, even if the Licensor has been advised of the possibility of such losses, costs, expenses, or damages. Where a limitation of liability is not allowed in full or in part, this limitation may not apply to You.__
c. The disclaimer of warranties and limitation of liability provided above shall be interpreted in a manner that, to the extent possible, most closely approximates an absolute disclaimer and waiver of all liability.
### Section 6 Term and Termination.
a. This Public License applies for the term of the Copyright and Similar Rights licensed here. However, if You fail to comply with this Public License, then Your rights under this Public License terminate automatically.
b. Where Your right to use the Licensed Material has terminated under Section 6(a), it reinstates:
1. automatically as of the date the violation is cured, provided it is cured within 30 days of Your discovery of the violation; or
2. upon express reinstatement by the Licensor.
For the avoidance of doubt, this Section 6(b) does not affect any right the Licensor may have to seek remedies for Your violations of this Public License.
c. For the avoidance of doubt, the Licensor may also offer the Licensed Material under separate terms or conditions or stop distributing the Licensed Material at any time; however, doing so will not terminate this Public License.
d. Sections 1, 5, 6, 7, and 8 survive termination of this Public License.
### Section 7 Other Terms and Conditions.
a. The Licensor shall not be bound by any additional or different terms or conditions communicated by You unless expressly agreed.
b. Any arrangements, understandings, or agreements regarding the Licensed Material not stated herein are separate from and independent of the terms and conditions of this Public License.
### Section 8 Interpretation.
a. For the avoidance of doubt, this Public License does not, and shall not be interpreted to, reduce, limit, restrict, or impose conditions on any use of the Licensed Material that could lawfully be made without permission under this Public License.
b. To the extent possible, if any provision of this Public License is deemed unenforceable, it shall be automatically reformed to the minimum extent necessary to make it enforceable. If the provision cannot be reformed, it shall be severed from this Public License without affecting the enforceability of the remaining terms and conditions.
c. No term or condition of this Public License will be waived and no failure to comply consented to unless expressly agreed to by the Licensor.
d. Nothing in this Public License constitutes or may be interpreted as a limitation upon, or waiver of, any privileges and immunities that apply to the Licensor or You, including from the legal processes of any jurisdiction or authority.
> Creative Commons is not a party to its public licenses. Notwithstanding, Creative Commons may elect to apply one of its public licenses to material it publishes and in those instances will be considered the “Licensor.” The text of the Creative Commons public licenses is dedicated to the public domain under the [CC0 Public Domain Dedication](https://creativecommons.org/publicdomain/zero/1.0/legalcode). Except for the limited purpose of indicating that material is shared under a Creative Commons public license or as otherwise permitted by the Creative Commons policies published at [creativecommons.org/policies](http://creativecommons.org/policies), Creative Commons does not authorize the use of the trademark “Creative Commons” or any other trademark or logo of Creative Commons without its prior written consent including, without limitation, in connection with any unauthorized modifications to any of its public licenses or any other arrangements, understandings, or agreements concerning use of licensed material. For the avoidance of doubt, this paragraph does not form part of the public licenses.
>
> Creative Commons may be contacted at creativecommons.org.
+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)
+213 -47
View File
@@ -1,63 +1,229 @@
# MQTT-Explorer
# [MQTT Explorer](https://mqtt-explorer.com)
[![Downloads](https://img.shields.io/github/release/thomasnordquist/mqtt-explorer.svg)](https://travis-ci.org/thomasnordquist/MQTT-Explorer/releases)
[![Downloads](https://img.shields.io/github/downloads/thomasnordquist/mqtt-explorer/total.svg)](https://travis-ci.org/thomasnordquist/MQTT-Explorer/releases)
[![Build Status](https://travis-ci.org/thomasnordquist/MQTT-Explorer.svg?branch=master)](https://travis-ci.org/thomasnordquist/MQTT-Explorer)
[![Build_Status](https://travis-ci.org/thomasnordquist/MQTT-Explorer.svg?branch=master)](https://travis-ci.org/thomasnordquist/MQTT-Explorer)
[![Build status](https://ci.appveyor.com/api/projects/status/c35tkm29rm4m5364/branch/master?svg=true)](https://ci.appveyor.com/project/thomasnordquist/mqtt-explorer/branch/master)
[![Codacy Badge](https://api.codacy.com/project/badge/Grade/47b26e03fce543ceac7914214482334a)](https://app.codacy.com/app/thomasnordquist/MQTT-Explorer?utm_source=github.com&utm_medium=referral&utm_content=thomasnordquist/MQTT-Explorer&utm_campaign=Badge_Grade_Dashboard)
### Version 0.1.3
See the whole picture of your message queue.
The perfect tool to integrate new services, IoT devices in your network.
This application subscribes to all topics on your MQTT-Server and displays your message queue hierarchy, allowing you to drill-down to the topics that are of interest.
| | | |
| :---------------------------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------: | :------------------------------------------------------------------------------------------------------------: |
| [![screen_composite](https://mqtt-explorer.com/img/screen-composite_small.png)](https://mqtt-explorer.com/img/screen-composite.png) | [![screen2_small](https://mqtt-explorer.com/img/screen2_small.png)](https://mqtt-explorer.com/img/screen2.png) | [![screen3_small](https://mqtt-explorer.com/img/screen3_small.png)](https://mqtt-explorer.com/img/screen3.png) |
## Download
The app is prebuilt for Windows ([portable](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.1.3&#x2F;MQTT-Explorer-0.1.3.exe), [installer](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.1.3&#x2F;MQTT-Explorer-Setup-0.1.3.exe)), Linux ([AppImage](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.1.3&#x2F;MQTT-Explorer-0.1.3-x86_64.AppImage)) and Mac ([dmg](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.1.3&#x2F;MQTT-Explorer-0.1.3.dmg)).
# The App has moved to [mqtt-explorer.com](https://mqtt-explorer.com)
| Platform | | Downloads |
|:----------|:-------------:|:------:|
| ![windows](https://user-images.githubusercontent.com/7721625/51445407-b4172080-1d04-11e9-8c70-d8413d1d6d8b.png) | Windows | [portable](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.1.3&#x2F;MQTT-Explorer-0.1.3.exe), [installer](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.1.3&#x2F;MQTT-Explorer-Setup-0.1.3.exe) |
| ![linux](https://user-images.githubusercontent.com/7721625/51445392-947ff800-1d04-11e9-8c7f-a30efb755651.png) | Linux | [AppImage](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.1.3&#x2F;MQTT-Explorer-0.1.3-x86_64.AppImage)<br>*Run AppImage:<br>Make it executable and double-click it.* |
| ![mac](https://user-images.githubusercontent.com/7721625/51445390-921d9e00-1d04-11e9-8339-351469ef20ae.png) | Mac | [dmg](https:&#x2F;&#x2F;github.com&#x2F;thomasnordquist&#x2F;MQTT-Explorer&#x2F;releases&#x2F;download&#x2F;v0.1.3&#x2F;MQTT-Explorer-0.1.3.dmg) |
MQTT Explorer is a comprehensive and easy-to-use MQTT Client.
Downloads can be found at the link above.
More architectures and package types: [Downloads](https://github.com/thomasnordquist/MQTT-Explorer/releases)
This page is dedicated to its development.
Pull-Requests and error reports are welcome.
![screen1](https://user-images.githubusercontent.com/7721625/51770198-6c6a0d80-20e5-11e9-94d5-a0174634253c.png)
## Quick Start with GitHub Codespaces
![screen2](https://user-images.githubusercontent.com/7721625/51770345-c7036980-20e5-11e9-94dc-5d6fa9dbf86b.png)
The fastest way to start developing is with GitHub Codespaces:
1. Click the green "Code" button above
2. Select "Codespaces" tab
3. Click "Create codespace on [branch]"
4. Wait for the environment to set up (includes Node.js and MQTT broker)
5. Run `yarn dev:server` to start development
## Develop
PRs and issues are welcome
The devcontainer includes a pre-configured MQTT broker and all development tools. See [.devcontainer/README.md](.devcontainer/README.md) for details.
Install with `npm run install`, build with `npm run build`
## Run from sources
Start with `npm run start`
### Desktop Application (Electron)
The `app` directory contains all the rendering logic, the `backend` directory currently contains the models, tests, connection management.
## Telemetry
The App sends telemetry and error reports, this enables me to quickly react on bugs/errors I produced.
This is a difficutlt task since this App runs on three different operating systems and architectures.
It basically sends: app version, processor architecture, operating system, used memory, user interactions and error stacks.
This greatly helps to improve the software quality and reliability.
No data about you or your data is send or stored.
Even thoug the data is purely technical, an option to disable telemetry is planned. [#52](https://github.com/thomasnordquist/MQTT-Explorer/issues/52)
Example telemetry:
```javascript
{ system: { arch: 'x64', platform: 'darwin' },
appVersion: '0.0.7',
events: { HELLO_EVENT: [ 1547714886134 ] },
now: 1547714886135,
transactionId: '1767d251-f492-4f2c-aa62-88add3acc26b' }
{ errors:
[ { time: 1547714887921,
message: 'He\'s dead Jim!',
stack:
'Error: He\'s dead Jim!\n at ./src/tracking.ts.exports.default (./mqtt-explorer/app/build/bundle.js:142765:11)\n at new Promise (<anonymous>)\n at Object../src/tracking.ts (./mqtt-explorer/app/build/bundle.js:142764:1)\n at __webpack_require__ (./mqtt-explorer/app/build/bundle.js:20:30)\n at Object../src/index.tsx (./mqtt-explorer/app/build/bundle.js:142618:1)\n at __webpack_require__ (./mqtt-explorer/app/build/bundle.js:20:30)\n at ../backend/node_modules/charenc/charenc.js.charenc.utf8.stringToBytes (./mqtt-explorer/app/build/bundle.js:84:18)\n at ./mqtt-explorer/app/build/bundle.js:87:10' } ],
now: 1547714887921,
transactionId: '53bf9aac-e695-40cc-9a81-b1cf3398843d' }
```bash
npm install -g yarn
yarn
yarn build
yarn start
```
### Browser Mode (Web Application)
MQTT Explorer can also run as a web application served by a Node.js server:
```bash
npm install -g yarn
yarn
yarn build:server
yarn start:server
```
Then open your browser to `http://localhost:3000`. For more details, see [BROWSER_MODE.md](BROWSER_MODE.md).
### Docker (Browser Mode)
[![Try in PWD](https://raw.githubusercontent.com/play-with-docker/stacks/master/assets/images/button.png)](https://labs.play-with-docker.com/?stack=https://raw.githubusercontent.com/thomasnordquist/MQTT-Explorer/master/docker-compose.yml)
Run MQTT Explorer in a Docker container:
```bash
docker run -d \
-p 3000:3000 \
-e MQTT_EXPLORER_USERNAME=admin \
-e MQTT_EXPLORER_PASSWORD=your_secure_password \
-v mqtt-explorer-data:/app/data \
ghcr.io/thomasnordquist/mqtt-explorer:latest
```
**Supports multiple platforms**: amd64, arm64 (Raspberry Pi 3/4/5), arm/v7 (Raspberry Pi 2/3).
**Enterprise integration**: Set `MQTT_EXPLORER_SKIP_AUTH=true` to disable built-in authentication when deploying behind a secure authentication proxy (e.g., OAuth2 Proxy, SSO).
For complete Docker documentation including authentication options, deployment examples, and security best practices, see [DOCKER.md](DOCKER.md).
## Develop
### Desktop Application
Launch Application
```bash
npm install -g yarn
yarn
yarn dev
```
### Browser Mode
Launch in development mode with hot reload:
```bash
npm install -g yarn
yarn
yarn dev:server
```
The `app` directory contains all the rendering logic, the `backend` directory currently contains the models, tests, connection management, `src` contains all the electron bindings. [mqttjs](https://github.com/mqttjs/MQTT.js) is used to facilitate communication to MQTT brokers.
## Automated Tests
MQTT Explorer uses multiple test suites to ensure reliability and quality:
### Unit Tests
**App tests** - Frontend component and logic tests:
```bash
yarn test:app
```
**Backend tests** - Data model and business logic tests:
```bash
yarn test:backend
```
**Run all unit tests**:
```bash
yarn test
```
### Integration & UI Tests
**UI test suite** - Independent, deterministic browser tests:
```bash
yarn build
yarn test:ui
```
**Demo video generation** - UI test recording for documentation:
```bash
yarn build
yarn test:demo-video
```
Note: Requires Xvfb, mosquitto broker, tmux, and ffmpeg. For full video recording setup, use `./scripts/uiTests.sh`.
**MCP introspection tests** - Model Context Protocol validation:
```bash
yarn build
yarn test:mcp
```
**Run all tests** (unit tests + demo video):
```bash
yarn build
yarn test:all
```
### Run UI Test Suite
The UI test suite validates core functionality through automated browser tests. Each test is independent and deterministic.
```bash
# Run with automated setup (recommended)
./scripts/runUiTests.sh
# Or run directly (requires manual MQTT broker setup)
yarn build
yarn test:ui
```
See [docs/UI-TEST-SUITE.md](docs/UI-TEST-SUITE.md) for more details.
### Run Demo Video Generation
The demo video is used for documentation and showcases key features. It requires additional dependencies:
```bash
yarn build
yarn test:demo-video
```
**Requirements:**
- mosquitto MQTT broker
- Xvfb (virtual framebuffer)
- tmux (terminal multiplexer)
- ffmpeg (video encoding)
**For full video recording with post-processing:**
```bash
yarn build
./scripts/uiTests.sh
```
This script handles Xvfb setup, mosquitto startup, video recording, and cleanup.
## Create a release
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.
## Write docs
```
git clone --single-branch -b gh-pages https://github.com/thomasnordquist/MQTT-Explorer.git mqtt-explorer-pages
cd mqtt-explorer-pages
bundle install
bundle exec jekyll serve --incremental
```
Readme file: `Readme.tpl.md`
Preview is available at
http://localhost:4000/Readme.tpl
## Update docs
```
npm install
./updateReadme.ts
```
The readme will be generated from the docs.
## License
Not yet decided which license exactly, but the basic idea is: "You may do whatever you want with this tool, except sell it."
![CC-BY-Nc 4.0](https://img.shields.io/badge/License-CC%20BY--NC%204.0-blue.svg)
[CC-BY-Nc 4.0](https://creativecommons.org/licenses/by-nC/4.0/)
The license allows for anyone to adapt, share, and redistribute the material, as long as it is non-commercial.
-63
View File
@@ -1,63 +0,0 @@
# MQTT-Explorer
[![Downloads](https://img.shields.io/github/release/thomasnordquist/mqtt-explorer.svg)](https://travis-ci.org/thomasnordquist/MQTT-Explorer/releases)
[![Downloads](https://img.shields.io/github/downloads/thomasnordquist/mqtt-explorer/total.svg)](https://travis-ci.org/thomasnordquist/MQTT-Explorer/releases)
[![Build Status](https://travis-ci.org/thomasnordquist/MQTT-Explorer.svg?branch=master)](https://travis-ci.org/thomasnordquist/MQTT-Explorer)
### Version {{ version }}
See the whole picture of your message queue.
The perfect tool to integrate new services, IoT devices in your network.
This application subscribes to all topics on your MQTT-Server and displays your message queue hierarchy, allowing you to drill-down to the topics that are of interest.
## Download
The app is prebuilt for Windows ({{windowsTargets}}), Linux ({{linuxTargets}}) and Mac ({{macTargets}}).
| Platform | | Downloads |
|:----------|:-------------:|:------:|
| ![windows](https://user-images.githubusercontent.com/7721625/51445407-b4172080-1d04-11e9-8c70-d8413d1d6d8b.png) | Windows | {{windowsTargets}} |
| ![linux](https://user-images.githubusercontent.com/7721625/51445392-947ff800-1d04-11e9-8c7f-a30efb755651.png) | Linux | {{linuxTargets}}<br>*Run AppImage:<br>Make it executable and double-click it.* |
| ![mac](https://user-images.githubusercontent.com/7721625/51445390-921d9e00-1d04-11e9-8339-351469ef20ae.png) | Mac | {{macTargets}} |
More architectures and package types: [Downloads](https://github.com/thomasnordquist/MQTT-Explorer/releases)
![screen1](https://user-images.githubusercontent.com/7721625/51770198-6c6a0d80-20e5-11e9-94d5-a0174634253c.png)
![screen2](https://user-images.githubusercontent.com/7721625/51770345-c7036980-20e5-11e9-94dc-5d6fa9dbf86b.png)
## Develop
PRs and issues are welcome
Install with `npm run install`, build with `npm run build`
Start with `npm run start`
The `app` directory contains all the rendering logic, the `backend` directory currently contains the models, tests, connection management.
## Telemetry
The App sends telemetry and error reports, this enables me to quickly react on bugs/errors I produced.
This is a difficutlt task since this App runs on three different operating systems and architectures.
It basically sends: app version, processor architecture, operating system, used memory, user interactions and error stacks.
This greatly helps to improve the software quality and reliability.
No data about you or your data is send or stored.
Even thoug the data is purely technical, an option to disable telemetry is planned. [#52](https://github.com/thomasnordquist/MQTT-Explorer/issues/52)
Example telemetry:
```javascript
{ system: { arch: 'x64', platform: 'darwin' },
appVersion: '0.0.7',
events: { HELLO_EVENT: [ 1547714886134 ] },
now: 1547714886135,
transactionId: '1767d251-f492-4f2c-aa62-88add3acc26b' }
{ errors:
[ { time: 1547714887921,
message: 'He\'s dead Jim!',
stack:
'Error: He\'s dead Jim!\n at ./src/tracking.ts.exports.default (./mqtt-explorer/app/build/bundle.js:142765:11)\n at new Promise (<anonymous>)\n at Object../src/tracking.ts (./mqtt-explorer/app/build/bundle.js:142764:1)\n at __webpack_require__ (./mqtt-explorer/app/build/bundle.js:20:30)\n at Object../src/index.tsx (./mqtt-explorer/app/build/bundle.js:142618:1)\n at __webpack_require__ (./mqtt-explorer/app/build/bundle.js:20:30)\n at ../backend/node_modules/charenc/charenc.js.charenc.utf8.stringToBytes (./mqtt-explorer/app/build/bundle.js:84:18)\n at ./mqtt-explorer/app/build/bundle.js:87:10' } ],
now: 1547714887921,
transactionId: '53bf9aac-e695-40cc-9a81-b1cf3398843d' }
```
## License
Not yet decided which license exactly, but the basic idea is: "You may do whatever you want with this tool, except sell it."
+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!
-1
View File
@@ -1 +0,0 @@
theme: jekyll-theme-architect
+182 -119
View File
@@ -1,134 +1,197 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>MQTT-Explorer</title>
<script src="./bugtracking.bundle.js"></script>
<style>
body, html {
margin: 0;
padding: 0;
}
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width, shrink-to-fit=no" />
<title>MQTT Explorer</title>
<script src="./bugtracking.bundle.js"></script>
@keyframes example {
0% {background-color: none;}
25% {background-color: #3f51b5;}
50% {background-color: #3f51b5;}
100% {background-color: none;}
}
<style>
body,
html {
margin: 0;
padding: 0;
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
[tabindex] {
outline: none;
}
::-webkit-scrollbar-corner {
background-color: rgba(0, 0, 0, 0.0);
}
@keyframes updateDark {
0% {
background-color: none;
}
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(30,30,30,0.3);
}
25% {
background-color: #595585;
}
::-webkit-scrollbar-thumb {
background-color: rgba(140,140,140,0.8);
}
</style>
<style>
#splash {
z-index: 1000000;
background-color: #303030;
display: block;
width: 100vw;
height: 100vh;
position: fixed;
opacity: 1;
}
#splash1 {
margin: 37vh auto 0 auto;
height: 25vh;
width: 25vh;
/* background-image:url('../rings.svg'); */
background-size: cover;
}
#splash2 {
margin: 0 auto;
}
@keyframes unsplash {
0% {opacity: 1;}
100% {opacity: 0;}
}
50% {
background-color: #595585;
}
.Resizer {
background: #eee;
opacity: .2;
z-index: 1;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
-moz-background-clip: padding;
-webkit-background-clip: padding;
background-clip: padding-box;
}
100% {
background-color: none;
}
}
.Resizer:hover {
-webkit-transition: all 0.3s ease-out;
transition: all 0.3s ease-out;
}
@keyframes updateLight {
0% {
background-color: none;
color: inherit;
}
.Resizer.horizontal {
height: 11px;
margin: -5px 0;
border-top: 5px solid rgba(255, 255, 255, 0);
border-bottom: 5px solid rgba(255, 255, 255, 0);
cursor: row-resize;
width: 100%;
}
25% {
background-color: #c0c8c0;
}
.Resizer.horizontal:hover {
border-top: 5px solid rgba(0, 0, 0, 0.5);
border-bottom: 5px solid rgba(0, 0, 0, 0.5);
}
50% {
background-color: #c0c8c0;
}
.Resizer.vertical {
width: 11px;
margin: 0 -5px;
border-left: 5px solid rgba(255, 255, 255, 0);
border-right: 5px solid rgba(255, 255, 255, 0);
cursor: col-resize;
}
100% {
background-color: none;
color: inherit;
}
}
.Resizer.vertical:hover {
border-left: 5px solid rgba(255, 255, 255, 0.5);
border-right: 5px solid rgba(255, 255, 255, 0.5);
}
.Resizer.disabled {
cursor: not-allowed;
}
.Resizer.disabled:hover {
border-color: transparent;
}
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
</style>
<script src="http://localhost:35729/livereload.js"></script>
</head>
<body>
<div id="splash"><div id="splash1"></div></div>
<div id="app" style="font:-webkit-control"></div>
<script>
function loadScript(path) {
var script = document.createElement("script");
script.src = path
document.head.appendChild(script);
}
document.addEventListener('DOMContentLoaded', onLoad(), false);
function onLoad() {
// <% _.forEach(htmlWebpackPlugin.files.js, function(file) { %>loadScript("<%- file %>");<% }); %>
// loadScript("<%= JSON.stringify(htmlWebpackPlugin) %>")
}
</script>
<% _.forEach(htmlWebpackPlugin.files.js, function(file) { %><script src="<%- file %>"></script><% }); %>
</body>
</html>
::-webkit-scrollbar-corner {
background-color: rgba(0, 0, 0, 0);
}
::-webkit-scrollbar-track {
-webkit-box-shadow: inset 0 0 6px rgba(60, 60, 60, 0.5);
background-color: rgba(140, 140, 140, 0.1);
}
::-webkit-scrollbar-thumb {
background-color: rgba(140, 140, 140, 0.8);
}
</style>
<style>
.Resizer {
background: rgba(200, 200, 200, 0);
z-index: 10;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
-moz-background-clip: padding;
-webkit-background-clip: padding;
background-clip: padding-box;
}
.Resizer:hover {
-webkit-transition: all 0.3s ease-out;
transition: all 0.3s ease-out;
}
.Resizer.horizontal {
height: 10px;
margin: -10px 0 0 0;
border-top: 5px solid rgba(255, 255, 255, 0);
border-bottom: 5px solid rgba(255, 255, 255, 0);
cursor: row-resize;
width: 100%;
}
.Resizer.horizontal::before {
content: '•••';
display: inline-block;
vertical-align: middle;
text-align: center;
width: 100%;
margin-top: -22px;
color: #aaa;
opacity: 1;
}
.Resizer.horizontal:hover {
border-top: 5px solid rgba(120, 120, 120, 0.3);
border-bottom: 5px solid rgba(120, 120, 120, 0.3);
}
.Resizer.vertical {
width: 2px;
margin: 0px -8px 0px 0px;
border-left: 0px solid rgba(128, 128, 128, 0);
border-right: 8px solid rgba(128, 128, 128, 0);
cursor: col-resize;
}
.Resizer.vertical::before {
content: '•••';
margin-left: -11px;
height: 3em;
margin-top: calc(50vh - 32px);
display: inline-block;
vertical-align: middle;
text-align: center;
color: #aaa;
opacity: 1;
writing-mode: vertical-lr;
text-orientation: sideways;
}
.Resizer.vertical:hover {
border-left: 0px solid rgba(130, 130, 130, 0.3);
border-right: 8px solid rgba(140, 140, 140, 0.3);
}
.Resizer.disabled {
cursor: not-allowed;
}
.Resizer.disabled:hover {
border-color: transparent;
}
.example-enter {
opacity: 0;
}
.example-enter-active {
opacity: 1;
transition: opacity 300ms ease-in;
}
.example-exit {
opacity: 1;
}
.example-exit-active {
opacity: 0;
transition: opacity 300ms ease-in;
}
</style>
<script>
global = globalThis //<- this should be enough
</script>
</head>
<body>
<div id="app" style="font: -webkit-control;"></div>
<script>
function loadScript(path) {
var script = document.createElement('script')
script.src = path
document.head.appendChild(script)
}
document.addEventListener('DOMContentLoaded', onLoad(), false)
function onLoad() {
// <% _.forEach(htmlWebpackPlugin.files.js, function(file) { %>loadScript("<%- file %>");<% }); %>
// loadScript("<%= JSON.stringify(htmlWebpackPlugin) %>")
}
</script>
<% _.forEach(htmlWebpackPlugin.files.js, function(file) { %>
<script src="<%- file %>"></script>
<% }); %>
</body>
</html>
+85 -42
View File
@@ -4,54 +4,97 @@
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack --mode production"
"build": "webpack --mode production",
"dev": "node_modules/.bin/webpack-dev-server --mode development --progress",
"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"
},
"author": "",
"license": "ISC",
"devDependencies": {
"@material-ui/core": "^3.9.0",
"@material-ui/icons": "^3.0.1",
"@material-ui/styles": "^3.0.0-alpha.8",
"@types/node": "^10.12.18",
"@types/react": "^16.7.18",
"@types/react-dom": "^16.0.11",
"@types/react-redux": "^6.0.12",
"@types/react-resize-detector": "^3.1.0",
"@types/react-split-pane": "^0.1.67",
"@types/sha1": "^1.1.1",
"@types/socket.io-client": "^1.4.32",
"@types/vis": "^4.21.9",
"awesome-typescript-loader": "^5.2.1",
"compare-versions": "^3.4.0",
"copy-text-to-clipboard": "^1.0.4",
"css-loader": "^2.1.0",
"electron-nucleus": "^1.11.0",
"electron-telemetry": "git+https://github.com/thomasnordquist/electron-telemetry.git#dist",
"html-webpack-plugin": "^4.0.0-beta.5",
"jquery": "^3.3.1",
"license": "CC-BY-ND-4.0",
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@mui/icons-material": "^7.3.6",
"@mui/lab": "^7.0.1-beta.20",
"@mui/material": "^7.3.6",
"@mui/styles": "^6.4.8",
"@types/react-transition-group": "^4.4.11",
"ace-builds": "^1.4.11",
"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.3.7",
"in-viewport": "^3.6.0",
"js-base64": "^3.7.8",
"json-to-ast": "^2.1.0",
"lodash.debounce": "^4.0.8",
"lodash.throttle": "^4.1.1",
"moving-average": "^1.0.0",
"number-abbreviate": "^2.0.0",
"react": "^16.8.0-alpha.1",
"react-ace": "^6.3.2",
"react-dom": "^16.7.0",
"react-json-view": "^1.19.1",
"react-redux": "^6.0.0",
"react-resize-detector": "^3.4.0",
"react-split-pane": "^0.1.85",
"react-vis": "^1.11.6",
"redux": "^4.0.1",
"redux-batched-actions": "^0.4.1",
"os-browserify": "^0.3.0",
"parse-duration": "^0.1.1",
"path-browserify": "^1.0.1",
"prismjs": "^1.29.0",
"react": "^19.2.3",
"react-ace": "^14.0.1",
"react-dom": "^19.2.3",
"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": "^2.2.0",
"source-map-loader": "^0.2.4",
"style-loader": "^0.23.1",
"typescript": "^3.2.2",
"webpack": "^4.28.2",
"webpack-bundle-analyzer": "^3.0.3",
"webpack-cli": "^3.1.2",
"webpack-dev-server": "^3.1.14",
"webpack-livereload-plugin": "^2.2.0"
"socket.io-client": "^4.8.1",
"url": "^0.11.4",
"uuid": "^11.0.0"
},
"devDependencies": {
"@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": "^19.2.7",
"@types/react-dom": "^19.2.3",
"@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": "^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.6.3",
"lodash": "^4.17.21",
"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": "^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"
}
}
-42
View File
@@ -1,42 +0,0 @@
<!-- By Sam Herbert (@sherb), for everyone. More @ http://goo.gl/7AJzbL -->
<svg width="45" height="45" viewBox="0 0 45 45" xmlns="http://www.w3.org/2000/svg" stroke="#fff">
<g fill="none" fill-rule="evenodd" transform="translate(1 1)" stroke-width="2">
<circle cx="22" cy="22" r="6" stroke-opacity="0">
<animate attributeName="r"
begin="1.5s" dur="3s"
values="6;22"
calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="stroke-opacity"
begin="1.5s" dur="3s"
values="1;0" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="stroke-width"
begin="1.5s" dur="3s"
values="2;0" calcMode="linear"
repeatCount="indefinite" />
</circle>
<circle cx="22" cy="22" r="6" stroke-opacity="0">
<animate attributeName="r"
begin="3s" dur="3s"
values="6;22"
calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="stroke-opacity"
begin="3s" dur="3s"
values="1;0" calcMode="linear"
repeatCount="indefinite" />
<animate attributeName="stroke-width"
begin="3s" dur="3s"
values="2;0" calcMode="linear"
repeatCount="indefinite" />
</circle>
<circle cx="22" cy="22" r="8">
<animate attributeName="r"
begin="0s" dur="1.5s"
values="6;1;2;3;4;5;6"
calcMode="linear"
repeatCount="indefinite" />
</circle>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

-124
View File
@@ -1,124 +0,0 @@
import * as React from 'react'
import * as q from '../../backend/src/Model'
import { Theme, withStyles } from '@material-ui/core/styles'
import { AppState } from './reducers'
import Connection from './components/ConnectionSetup/Connection'
import CssBaseline from '@material-ui/core/CssBaseline'
const Settings = React.lazy(() => import('./components/Settings'))
import Sidebar from './components/Sidebar/Sidebar'
import TitleBar from './components/TitleBar'
import Tree from './components/Tree/Tree'
import UpdateNotifier from './UpdateNotifier'
import { connect } from 'react-redux'
import ErrorBoundary from './ErrorBoundary'
import { default as SplitPane } from 'react-split-pane'
interface Props {
name: string
connectionId: string
classes: any
settingsVisible: boolean
}
class App extends React.PureComponent<Props, {}> {
constructor(props: any) {
super(props)
this.state = { }
}
public render() {
const { settingsVisible } = this.props
const { content, contentShift, centerContent, paneDefaults, heightProperty } = this.props.classes
return (
<div className={centerContent}>
<CssBaseline />
<ErrorBoundary>
<React.Suspense fallback={<div>Loading...</div>}>
<Settings />
</React.Suspense>
<div className={centerContent}>
<div className={`${settingsVisible ? contentShift : content}`}>
<TitleBar />
</div>
<div>
<SplitPane
step={48}
primary="second"
className={`${settingsVisible ? contentShift : content} ${heightProperty}`}
split="vertical"
minSize={250}
defaultSize={500}
allowResize={true}
style={{ position: 'relative' }}
pane1Style={{ overflow: 'hidden' }}
>
<div className={paneDefaults}>
<Tree />
</div>
<div className={paneDefaults}>
<Sidebar connectionId={this.props.connectionId} />
</div>
</SplitPane>
</div>
</div>
<UpdateNotifier />
<Connection />
</ErrorBoundary>
</div >
)
}
}
const mapStateToProps = (state: AppState) => {
return {
settingsVisible: state.settings.visible,
connectionId: state.connection.connectionId,
}
}
const styles = (theme: Theme) => {
const drawerWidth = 300
return {
heightProperty: {
height: 'calc(100vh - 64px) !important',
},
paneDefaults: {
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
overflowY: 'scroll' as 'scroll',
overflowX: 'hidden' as 'hidden',
display: 'block' as 'block',
height: 'calc(100vh - 64px)',
},
centerContent: {
width: '100vw',
overflow: 'hidden' as 'hidden',
},
content: {
width: '100vw',
overflowX: 'hidden' as 'hidden',
backgroundColor: theme.palette.background.default,
transition: theme.transitions.create('transform', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
transform: 'translateX(0px)',
},
contentShift: {
overflowX: 'hidden' as 'hidden',
width: '100vw',
padding: 0,
backgroundColor: theme.palette.background.default,
transition: theme.transitions.create('transform', {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
}),
transform: `translateX(${drawerWidth}px)`,
},
}
}
export default withStyles(styles)(connect(mapStateToProps)(App))
-19
View File
@@ -1,19 +0,0 @@
import { EventDispatcher } from '../../events'
export class TopicViewModel {
private selected: boolean
public change = new EventDispatcher<void, TopicViewModel>(this)
public constructor() {
this.selected = false
}
public isSelected() {
return this.selected
}
public setSelected(selected: boolean) {
this.selected = selected
this.change.dispatch()
}
}
+118
View File
@@ -0,0 +1,118 @@
import { Action, ActionTypes, ChartParameters } from '../reducers/Charts'
import { AppState } from '../reducers'
import { default as persistentStorage, StorageIdentifier } from '../utils/PersistentStorage'
import { Dispatch } from 'redux'
import { showError, showNotification } from './Global'
interface ConnectionViewState {
charts: Array<ChartParameters>
}
interface ConnectionViewStateDictionary {
[s: string]: ConnectionViewState
}
const connectionViewStateIdentifier: StorageIdentifier<ConnectionViewStateDictionary> = {
id: 'connection_view_state',
}
export const loadCharts = () => async (dispatch: Dispatch<any>, getState: () => AppState) => {
const connectionId = getState().connection.connectionId
if (!connectionId) {
return
}
let viewStates: ConnectionViewStateDictionary | undefined
try {
viewStates = await persistentStorage.load(connectionViewStateIdentifier)
} catch (error) {
dispatch(showError(error))
}
if (!viewStates || !viewStates[connectionId]) {
dispatch(setCharts([]))
return
}
const viewState = viewStates[connectionId]
if (viewState) {
dispatch(setCharts(viewState.charts))
}
}
export const saveCharts = () => async (dispatch: Dispatch<any>, getState: () => AppState) => {
const connectionId = getState().connection.connectionId
if (!connectionId) {
return
}
const charts = getState().charts.get('charts').toArray()
let viewStates: ConnectionViewStateDictionary | undefined
try {
viewStates = (await persistentStorage.load(connectionViewStateIdentifier)) || {}
const state: ConnectionViewState = viewStates[connectionId] || { charts: [] }
state.charts = charts
viewStates[connectionId] = state
await persistentStorage.store(connectionViewStateIdentifier, viewStates)
} catch (error) {
dispatch(showError(error))
}
}
export const addChart =
(chartParameters: ChartParameters) => async (dispatch: Dispatch<any>, getState: () => AppState) => {
const chartExists = Boolean(
getState()
.charts.get('charts')
.find(chart => chart.topic === chartParameters.topic && chart.dotPath === chartParameters.dotPath)
)
if (chartExists) {
dispatch(showNotification('Already added'))
return
}
dispatch({
type: ActionTypes.CHARTS_ADD,
chart: chartParameters,
})
dispatch(saveCharts())
dispatch(showNotification('Added to chart panel'))
}
export const updateChart =
(chartParameters: ChartParameters) => async (dispatch: Dispatch<any>, getState: () => AppState) => {
dispatch({
type: ActionTypes.CHARTS_UPDATE,
topic: chartParameters.topic,
dotPath: chartParameters.dotPath,
parameters: chartParameters,
})
dispatch(saveCharts())
}
export const removeChart =
(chartParameters: ChartParameters) => async (dispatch: Dispatch<any>, getState: () => AppState) => {
dispatch({
chart: chartParameters,
type: ActionTypes.CHARTS_REMOVE,
})
dispatch(saveCharts())
}
export const moveChartUp =
(parameters: { topic: string; dotPath?: string }) => async (dispatch: Dispatch<any>, getState: () => AppState) => {
dispatch({
topic: parameters.topic,
dotPath: parameters.dotPath,
type: ActionTypes.CHARTS_MOVE_UP,
})
dispatch(saveCharts())
}
export const setCharts = (charts: Array<ChartParameters>): Action => {
return {
charts,
type: ActionTypes.CHARTS_SET,
}
}
+70 -33
View File
@@ -1,55 +1,92 @@
import { ActionTypes, Action, ConnectionState } from '../reducers/Connection'
import { MqttOptions } from '../../../backend/src/DataSource'
import { Dispatch } from 'redux'
import { rendererEvents, addMqttConnectionEvent, makeConnectionStateEvent, removeConnection } from '../../../events'
import { AppState } from '../reducers'
import * as q from '../../../backend/src/Model'
import { showTree } from './Tree'
import * as url from 'url'
import { TopicViewModel } from '../TopicViewModel'
import { Action, ActionTypes } from '../reducers/Connection'
import { ActionTypes as SettingsActionTypes } from '../reducers/Settings'
import { AppState } from '../reducers'
import { DataSourceState, MqttOptions } from '../../../backend/src/DataSource'
import { Dispatch } from 'redux'
import { globalActions } from '.'
import { resetStore as resetTreeStore, showTree } from './Tree'
import { showError } from './Global'
import { TopicViewModel } from '../model/TopicViewModel'
import { addMqttConnectionEvent, makeConnectionStateEvent, removeConnection, rendererEvents } from '../../../events'
export const connect = (options: MqttOptions, connectionId: string) => (dispatch: Dispatch<any>, getState: () => AppState) => {
dispatch(connecting(connectionId))
rendererEvents.emit(addMqttConnectionEvent, { options, id: connectionId })
const event = makeConnectionStateEvent(connectionId)
const host = url.parse(options.url).hostname
export const connect =
(options: MqttOptions, connectionId: string) => (dispatch: Dispatch<any>, getState: () => AppState) => {
dispatch(connecting(connectionId))
rendererEvents.emit(addMqttConnectionEvent, { options, id: connectionId })
const event = makeConnectionStateEvent(connectionId)
const host = url.parse(options.url).hostname
rendererEvents.subscribe(event, (dataSourceState) => {
if (dataSourceState.connected) {
const tree = new q.Tree<TopicViewModel>()
tree.updateWithConnection(rendererEvents, connectionId)
dispatch(connected(tree, host!))
dispatch(showTree(tree))
} else if (dataSourceState.error) {
dispatch(showError(dataSourceState.error))
dispatch(disconnect())
}
rendererEvents.subscribe(event, dataSourceState => {
if (dataSourceState.connected) {
const didReconnect = Boolean(getState().connection.tree)
if (!didReconnect) {
const tree = new q.Tree<TopicViewModel>()
tree.updateWithConnection(rendererEvents, connectionId)
dispatch(showTree(tree))
dispatch(connected(tree, host!))
}
} else if (dataSourceState.error) {
dispatch(showError(dataSourceState.error))
dispatch(disconnect())
}
dispatch(updateHealth(dataSourceState))
})
}
const updateHealth = (dataSourceState: DataSourceState) => (dispatch: Dispatch<any>, getState: () => AppState) => {
let state
if (dataSourceState.connecting) {
state = 'connecting'
} else if (!dataSourceState.connected) {
state = 'offline'
dispatch(globalActions.showError('Disconnected from server'))
} else if (dataSourceState.connected) {
state = 'online'
} else {
state = undefined
}
dispatch({
type: ActionTypes.CONNECTION_SET_HEALTH,
health: state,
})
}
export const connected: (tree: q.Tree<TopicViewModel>, host: string) => Action = (tree: q.Tree<TopicViewModel>, host: string) => ({
export const connected: (tree: q.Tree<TopicViewModel>, host: string) => Action = (
tree: q.Tree<TopicViewModel>,
host: string
) => ({
tree,
host,
type: ActionTypes.CONNECTION_SET_CONNECTED,
})
export const connecting: (connectionId: string) => Action = (connectionId: string) => ({
export const connecting: (connectionId: string) => Action = (connectionId: string) => ({
connectionId,
type: ActionTypes.CONNECTION_SET_CONNECTING,
})
export const showError = (error?: string) => ({
error,
type: ActionTypes.CONNECTION_SET_SHOW_ERROR,
})
export const disconnect = () => (dispatch: Dispatch<any>, getState: () => AppState) => {
export const disconnect = () => (dispatch: Dispatch<any>, getState: () => AppState) => {
const { connectionId, tree } = getState().connection
rendererEvents.emit(removeConnection, connectionId)
tree && tree.stopUpdating()
if (connectionId) {
rendererEvents.emit(removeConnection, connectionId)
rendererEvents.unsubscribeAll(makeConnectionStateEvent(connectionId))
}
dispatch(showTree(undefined))
tree && tree.stopUpdating()
tree && tree.destroy()
// Clear topic filter
dispatch({
topicFilter: '',
type: SettingsActionTypes.SETTINGS_FILTER_TOPICS,
})
dispatch(resetTreeStore())
dispatch({
type: ActionTypes.CONNECTION_SET_DISCONNECTED,
})
dispatch(showTree(undefined))
}
+182
View File
@@ -0,0 +1,182 @@
import { AppState } from '../reducers'
import { clearLegacyConnectionOptions, loadLegacyConnectionOptions } from '../model/LegacyConnectionSettings'
import {
ConnectionOptions,
createEmptyConnection,
makeDefaultConnections,
CertificateParameters,
} from '../model/ConnectionOptions'
import { default as persistentStorage, StorageIdentifier } from '../utils/PersistentStorage'
import { Dispatch } from 'redux'
import { showError } from './Global'
import * as path from 'path'
import { ActionTypes, Action } from '../reducers/ConnectionManager'
import { Subscription } from '../../../backend/src/DataSource/MqttSource'
import { connectionsMigrator } from './migrations/Connection'
import { rendererRpc, readFromFile } from '../../../events'
import { makeOpenDialogRpc } from '../../../events/OpenDialogRequest'
export interface ConnectionDictionary {
[s: string]: ConnectionOptions
}
const storedConnectionsIdentifier: StorageIdentifier<ConnectionDictionary> = {
id: 'ConnectionManager_connections',
}
export const loadConnectionSettings = () => async (dispatch: Dispatch<any>, getState: () => AppState) => {
let connections
try {
await ensureConnectionsHaveBeenInitialized()
connections = await persistentStorage.load(storedConnectionsIdentifier)
// Apply migrations
if (connections && connectionsMigrator.isMigrationNecessary(connections)) {
connections = connectionsMigrator.applyMigrations(connections)
await persistentStorage.store(storedConnectionsIdentifier, connections)
}
} catch (error) {
dispatch(showError(error))
}
if (!connections) {
return
}
dispatch(setConnections(connections))
const firstKey = Object.keys(connections)[0]
if (firstKey) {
dispatch(selectConnection(firstKey))
}
}
export type CertificateTypes = 'selfSignedCertificate' | 'clientCertificate' | 'clientKey'
export const selectCertificate =
(type: CertificateTypes, connectionId: string) => async (dispatch: Dispatch<any>, getState: () => AppState) => {
try {
const certificate = await openCertificate()
dispatch(
updateConnection(connectionId, {
[type]: certificate,
})
)
} catch (error) {
dispatch(showError(error))
}
}
async function openCertificate(): Promise<CertificateParameters> {
const rejectReasons = {
noCertificateSelected: 'No certificate selected',
certificateSizeDoesNotMatch: 'Certificate size larger/smaller then expected.',
}
const openDialogReturnValue = await rendererRpc.call(makeOpenDialogRpc(), {
properties: ['openFile'],
securityScopedBookmarks: true,
})
const selectedFile = openDialogReturnValue.filePaths && openDialogReturnValue.filePaths[0]
if (!selectedFile) {
throw rejectReasons.noCertificateSelected
}
const data = await rendererRpc.call(readFromFile, { filePath: selectedFile })
if (data.length > 16_384 || data.length < 64) {
throw rejectReasons.certificateSizeDoesNotMatch
}
return {
data: data.toString('base64'),
name: path.basename(selectedFile),
}
}
export const saveConnectionSettings = () => async (dispatch: Dispatch<any>, getState: () => AppState) => {
try {
console.log('store settings')
await persistentStorage.store(storedConnectionsIdentifier, getState().connectionManager.connections)
} catch (error) {
dispatch(showError(error))
}
}
export const updateConnection = (connectionId: string, changeSet: Partial<ConnectionOptions>): Action => ({
connectionId,
changeSet,
type: ActionTypes.CONNECTION_MANAGER_UPDATE_CONNECTION,
})
export const addSubscription = (subscription: Subscription, connectionId: string): Action => ({
connectionId,
subscription,
type: ActionTypes.CONNECTION_MANAGER_ADD_SUBSCRIPTION,
})
export const deleteSubscription = (subscription: Subscription, connectionId: string): Action => ({
connectionId,
subscription,
type: ActionTypes.CONNECTION_MANAGER_DELETE_SUBSCRIPTION,
})
export const createConnection = () => (dispatch: Dispatch<any>) => {
const newConnection = createEmptyConnection()
dispatch(addConnection(newConnection))
dispatch(selectConnection(newConnection.id))
}
export const setConnections = (connections: { [s: string]: ConnectionOptions }): Action => ({
connections,
type: ActionTypes.CONNECTION_MANAGER_SET_CONNECTIONS,
})
export const selectConnection = (connectionId: string): Action => ({
selected: connectionId,
type: ActionTypes.CONNECTION_MANAGER_SELECT_CONNECTION,
})
export const addConnection = (connection: ConnectionOptions): Action => ({
connection,
type: ActionTypes.CONNECTION_MANAGER_ADD_CONNECTION,
})
export const toggleAdvancedSettings = (): Action => ({
type: ActionTypes.CONNECTION_MANAGER_TOGGLE_ADVANCED_SETTINGS,
})
export const toggleCertificateSettings = (): Action => ({
type: ActionTypes.CONNECTION_MANAGER_TOGGLE_CERTIFICATE_SETTINGS,
})
export const deleteConnection = (connectionId: string) => (dispatch: Dispatch<any>, getState: () => AppState) => {
const connectionIds = Object.keys(getState().connectionManager.connections)
const connectionIdLocation = connectionIds.indexOf(connectionId)
const remainingIds = connectionIds.filter(id => id !== connectionId)
const nextSelectedConnectionIndex = Math.min(remainingIds.length - 1, connectionIdLocation)
const nextSelectedConnection = remainingIds[nextSelectedConnectionIndex]
dispatch({
connectionId,
type: ActionTypes.CONNECTION_MANAGER_DELETE_CONNECTION,
})
if (nextSelectedConnection) {
dispatch(selectConnection(nextSelectedConnection))
}
}
async function ensureConnectionsHaveBeenInitialized() {
let connections = await persistentStorage.load(storedConnectionsIdentifier)
const requiresInitialization = !connections
if (requiresInitialization) {
const migratedConnection = loadLegacyConnectionOptions()
const defaultConnections = makeDefaultConnections()
connections = {
...migratedConnection,
...defaultConnections,
}
await persistentStorage.store(storedConnectionsIdentifier, connections)
clearLegacyConnectionOptions()
}
}
+49
View File
@@ -0,0 +1,49 @@
import { ActionTypes, ConfirmationRequest } from '../reducers/Global'
import { Dispatch } from 'redux'
export const showError = (error?: string | unknown) => ({
error,
type: ActionTypes.showError,
})
export const showNotification = (notification?: string) => ({
notification,
type: ActionTypes.showNotification,
})
export const didLaunch = () => ({
type: ActionTypes.didLaunch,
})
export const toggleSettingsVisibility = () => (dispatch: Dispatch<any>) => {
dispatch({
type: ActionTypes.toggleSettingsVisibility,
})
}
export const requestConfirmation = (title: string, inquiry: string) => (dispatch: Dispatch<any>) => {
return new Promise(resolve => {
const confirmationRequest = {
title,
inquiry,
callback: (confirmed: boolean) => {
resolve(confirmed)
dispatch(removeConfirmationRequest(confirmationRequest))
},
}
dispatch({
confirmationRequest,
type: ActionTypes.requestConfirmation,
})
})
}
export const removeConfirmationRequest = (confirmationRequest: ConfirmationRequest) => (dispatch: Dispatch<any>) => {
return new Promise((resolve, reject) => {
dispatch({
confirmationRequest,
type: ActionTypes.removeConfirmationRequest,
})
})
}
+55 -7
View File
@@ -1,15 +1,63 @@
import { ActionTypes, Action } from '../reducers/Publish'
import { Action, ActionTypes } from '../reducers/Publish'
import { AppState } from '../reducers'
import { Base64Message } from '../../../backend/src/Model/Base64Message'
import { Dispatch } from 'redux'
import { rendererEvents, makePublishEvent } from '../../../events'
import { MqttMessage, makePublishEvent, rendererEvents, rendererRpc, readFromFile } from '../../../events'
import { makeOpenDialogRpc } from '../../../events/OpenDialogRequest'
import { showError } from './Global'
import { Base64 } from 'js-base64'
export const setTopic = (topic?: string): Action => {
export const setTopic = (topic?: string): Action => {
return {
topic,
type: ActionTypes.PUBLISH_SET_TOPIC,
}
}
export const openFile =
(encoding: BufferEncoding = 'utf8') =>
async (dispatch: Dispatch<any>, getState: () => AppState) => {
try {
const file = await getFileContent(encoding)
if (file) {
dispatch(setPayload(file.data))
}
} catch (error) {
dispatch(showError(error))
}
}
type FileParameters = {
name: string
data: string
}
async function getFileContent(encoding: BufferEncoding): Promise<FileParameters | undefined> {
const rejectReasons = {
noFileSelected: 'No file selected',
errorReadingFile: 'Error reading file',
}
const { canceled, filePaths } = await rendererRpc.call(makeOpenDialogRpc(), {
properties: ['openFile'],
securityScopedBookmarks: true,
})
if (canceled) {
return
}
const selectedFile = filePaths[0]
if (!selectedFile) {
throw rejectReasons.noFileSelected
}
try {
const data = await rendererRpc.call(readFromFile, { filePath: selectedFile, encoding })
return { name: selectedFile, data: data.toString(encoding) }
} catch (error) {
throw rejectReasons.errorReadingFile
}
}
export const setPayload = (payload?: string): Action => {
return {
payload,
@@ -31,18 +79,18 @@ export const setEditorMode = (editorMode: string): Action => {
}
}
export const publish = (connectionId: string) => (dispatch: Dispatch<Action>, getState: () => AppState) => {
export const publish = (connectionId: string) => (dispatch: Dispatch<Action>, getState: () => AppState) => {
const state = getState()
const topic = state.publish.topic
const topic = state.publish.manualTopic ?? state.tree.get('selectedTopic')?.path()
if (!topic) {
return
}
const publishEvent = makePublishEvent(connectionId)
const mqttMessage = {
const mqttMessage: Partial<MqttMessage> = {
topic,
payload: state.publish.payload,
payload: state.publish.payload ? Base64Message.fromString(state.publish.payload) : null,
retain: state.publish.retain,
qos: state.publish.qos,
}
+109 -27
View File
@@ -1,34 +1,100 @@
import { Action, ActionTypes, TopicOrder } from '../reducers/Settings'
import { ActionTypes as TreeActionTypes } from '../reducers/Tree'
import { Dispatch } from 'redux'
import { showTree } from './Tree'
import { AppState } from '../reducers'
import * as q from '../../../backend/src/Model'
import { ActionTypes, SettingsStateModel, TopicOrder, ValueRendererDisplayMode } from '../reducers/Settings'
import { AppState } from '../reducers'
import { autoExpandLimitSet } from '../components/SettingsDrawer/Settings'
import { Base64Message } from '../../../backend/src/Model/Base64Message'
import { batchActions } from 'redux-batched-actions'
import { autoExpandLimitSet } from '../components/Settings'
import { TopicViewModel } from '../TopicViewModel'
import { default as persistentStorage, StorageIdentifier } from '../utils/PersistentStorage'
import { Dispatch } from 'redux'
import { globalActions } from './'
import { showError } from './Global'
import { showTree } from './Tree'
import { TopicViewModel } from '../model/TopicViewModel'
export const setAutoExpandLimit = (autoExpandLimit: number = 0): Action => {
return {
autoExpandLimit,
type: ActionTypes.SETTINGS_SET_AUTO_EXPAND_LIMIT,
const settingsIdentifier: StorageIdentifier<Partial<SettingsStateModel>> = {
id: 'Settings',
}
export const loadSettings = () => async (dispatch: Dispatch<any>, getState: () => AppState) => {
try {
const settings = (await persistentStorage.load(settingsIdentifier)) || {}
dispatch({
settings: getState().settings.merge(settings),
type: ActionTypes.SETTINGS_DID_LOAD_SETTINGS,
})
} catch (error) {
dispatch(showError(error))
}
dispatch(globalActions.didLaunch())
}
export const storeSettings = () => async (dispatch: Dispatch<any>, getState: () => AppState) => {
const settings = {
...getState().settings.toJS(),
autoExpandLimit: undefined,
topicFilter: undefined,
visible: undefined,
}
try {
await persistentStorage.store(settingsIdentifier, settings)
} catch (error) {
dispatch(showError(error))
}
}
export const toggleSettingsVisibility = (): Action => {
return {
type: ActionTypes.SETTINGS_TOGGLE_VISIBILITY,
export const setAutoExpandLimit =
(autoExpandLimit: number = 0) =>
(dispatch: Dispatch<any>) => {
dispatch({
autoExpandLimit,
type: ActionTypes.SETTINGS_SET_AUTO_EXPAND_LIMIT,
})
}
export const setTimeLocale = (timeLocale: string) => (dispatch: Dispatch<any>) => {
dispatch({
timeLocale,
type: ActionTypes.SETTINGS_SET_TIME_LOCALE,
})
dispatch(storeSettings())
}
export const setTopicOrder = (topicOrder: TopicOrder = TopicOrder.none): Action => {
return {
topicOrder,
type: ActionTypes.SETTINGS_SET_TOPIC_ORDER,
}
export const selectTopicWithMouseOver = (doSelect: boolean) => (dispatch: Dispatch<any>) => {
dispatch({
selectTopicWithMouseOver: doSelect,
type: ActionTypes.SETTINGS_SET_SELECT_TOPIC_WITH_MOUSE_OVER,
})
dispatch(storeSettings())
}
export const filterTopics = (filterStr: string) => (dispatch: Dispatch<any>, getState: () => AppState) => {
export const setValueDisplayMode =
(valueRendererDisplayMode: ValueRendererDisplayMode) => (dispatch: Dispatch<any>) => {
dispatch({
valueRendererDisplayMode,
type: ActionTypes.SETTINGS_SET_VALUE_RENDERER_DISPLAY_MODE,
})
dispatch(storeSettings())
}
export const toggleHighlightTopicUpdates = () => (dispatch: Dispatch<any>) => {
dispatch({
type: ActionTypes.SETTINGS_TOGGLE_HIGHLIGHT_ACTIVITY,
})
dispatch(storeSettings())
}
export const setTopicOrder =
(topicOrder: TopicOrder = TopicOrder.none) =>
(dispatch: Dispatch<any>) => {
dispatch({
topicOrder,
type: ActionTypes.SETTINGS_SET_TOPIC_ORDER,
})
dispatch(storeSettings())
}
export const filterTopics = (filterStr: string) => (dispatch: Dispatch<any>, getState: () => AppState) => {
const { tree } = getState().connection
dispatch({
@@ -36,8 +102,8 @@ export const filterTopics = (filterStr: string) => (dispatch: Dispatch<any>, get
type: ActionTypes.SETTINGS_FILTER_TOPICS,
})
if (!filterStr || !tree) {
dispatch(batchActions([setAutoExpandLimit(0), (showTree(tree) as any)]))
if (!filterStr || !tree) {
dispatch(batchActions([setAutoExpandLimit(0), showTree(tree) as any]))
return
}
@@ -49,11 +115,16 @@ export const filterTopics = (filterStr: string) => (dispatch: Dispatch<any>, get
return true
}
const messageMatches = (node.message && typeof node.message.value === 'string' && node.message.value.toLowerCase().indexOf(filterStr) !== -1)
const messageMatches =
node.message &&
node.message.payload &&
node.message.payload.toUnicodeString().toLowerCase().indexOf(filterStr) !== -1
return Boolean(messageMatches)
}
const resultTree = tree.childTopics()
const resultTree = tree
.childTopics()
.filter(nodeFilter)
.map((node: q.TreeNode<TopicViewModel>) => {
const clone = node.unconnectedClone()
@@ -70,16 +141,17 @@ export const filterTopics = (filterStr: string) => (dispatch: Dispatch<any>, get
nextTree.updateWithConnection(tree.updateSource, tree.connectionId, nodeFilter)
}
dispatch(batchActions([setAutoExpandLimit(autoExpandLimitForTree(nextTree)), (showTree(nextTree) as any)]))
dispatch(batchActions([setAutoExpandLimit(autoExpandLimitForTree(nextTree)), showTree(nextTree) as any]))
}
function autoExpandLimitForTree(tree: q.Tree<TopicViewModel>) {
if (!tree) {
return 0
}
function closestExistingLimit(i: number): number {
const sorted = autoExpandLimitSet.sort((a, b) => Math.abs(a.limit - i) - Math.abs(b.limit - i))
return sorted[0]!.limit
const sorted = [...autoExpandLimitSet].sort((a, b) => Math.abs(a.limit - i) - Math.abs(b.limit - i))
return sorted[0].limit
}
const count = tree.childTopicCount()
@@ -87,3 +159,13 @@ function autoExpandLimitForTree(tree: q.Tree<TopicViewModel>) {
return closestExistingLimit(calculatedLimit)
}
export const toggleTheme = () => (dispatch: Dispatch<any>, getState: () => AppState) => {
dispatch({
type:
getState().settings.get('theme') === 'light'
? ActionTypes.SETTINGS_SET_THEME_DARK
: ActionTypes.SETTINGS_SET_THEME_LIGHT,
})
dispatch(storeSettings())
}
+16 -14
View File
@@ -1,21 +1,23 @@
import { Dispatch, Action } from 'redux'
import * as q from '../../../backend/src/Model'
import { ActionTypes } from '../reducers/Sidebar'
import { AppState } from '../reducers'
import { makePublishEvent, rendererEvents } from '../../../events'
import { Dispatch } from 'redux'
import { clearTopic } from './clearTopic'
export const clearRetainedTopic = () => (dispatch: Dispatch<Action>, getState: () => AppState) => {
const { selectedTopic } = getState().tree
const { connectionId } = getState().connection
export { clearTopic } from './clearTopic'
if (!selectedTopic || !connectionId) {
export const clearRetainedTopic = () => (dispatch: Dispatch<any>, getState: () => AppState) => {
const selectedTopic = getState().tree.get('selectedTopic')
if (!selectedTopic) {
return
}
const publishEvent = makePublishEvent(connectionId)
const mqttMessage = {
topic: selectedTopic.path(),
payload: null,
retain: true,
qos: 0 as 0,
}
rendererEvents.emit(publishEvent, mqttMessage)
dispatch(clearTopic(selectedTopic, false))
}
export const setCompareMessage = (message?: q.Message) => (dispatch: Dispatch<any>) => {
dispatch({
message,
type: ActionTypes.SIDEBAR_SET_COMPARE_MESSAGE,
})
}
+96 -41
View File
@@ -1,59 +1,114 @@
import { AppState } from '../reducers'
import { ActionTypes } from '../reducers/Tree'
import * as q from '../../../backend/src/Model'
import { Dispatch, AnyAction } from 'redux'
import { setTopic } from './Publish'
import { TopicViewModel } from '../TopicViewModel'
import { ActionTypes } from '../reducers/Tree'
import { ActionTypes as SidebarActionTypes } from '../reducers/Sidebar'
import { AnyAction, Dispatch } from 'redux'
import { AppState } from '../reducers'
import { batchActions } from 'redux-batched-actions'
const debounce = require('lodash.debounce')
import { globalActions } from './'
import { setTopic } from './Publish'
import { TopicViewModel } from '../model/TopicViewModel'
import debounce from 'lodash.debounce'
export { clearTopic } from './clearTopic'
export const selectTopic = (topic: q.TreeNode<TopicViewModel>) => (dispatch: Dispatch<any>, getState: () => AppState) => {
debouncedSelectTopic(topic, dispatch, getState)
}
export { moveSelectionUpOrDownwards, moveInward, moveOutward } from './visibleTreeTraversal'
const debouncedSelectTopic = debounce((topic: q.TreeNode<TopicViewModel>, dispatch: Dispatch<any>, getState: () => AppState) => {
const { selectedTopic } = getState().tree
if (selectedTopic === topic) {
return
export const selectTopic =
(topic: q.TreeNode<TopicViewModel>) => (dispatch: Dispatch<any>, getState: () => AppState) => {
debouncedSelectTopic(topic, dispatch, getState)
}
// Update publish topic
let setTopicDispatch: any | undefined
if (selectedTopic && (selectedTopic.path() === getState().publish.topic || !getState().publish.topic)) {
setTopicDispatch = setTopic(topic.path())
}
const debouncedSelectTopic = debounce(
(topic: q.TreeNode<TopicViewModel>, dispatch: Dispatch<any>, getState: () => AppState) => {
const previouslySelectedTopic = getState().tree.get('selectedTopic')
if (selectedTopic && selectedTopic.viewModel) {
selectedTopic.viewModel.setSelected(false)
}
if (previouslySelectedTopic === topic) {
return
}
if (topic.viewModel) {
topic.viewModel.setSelected(true)
}
// Update publish topic
let setTopicDispatch: any | undefined
if (!getState().publish.manualTopic) {
setTopicDispatch = setTopic(topic.path())
} else if (previouslySelectedTopic && previouslySelectedTopic.path() === getState().publish.manualTopic) {
setTopicDispatch = setTopic(topic.path())
}
const selectTreeTopicDispatch = {
selectedTopic: topic,
type: ActionTypes.TREE_SELECT_TOPIC,
}
previouslySelectedTopic?.viewModel?.setSelected(false)
topic.viewModel?.setSelected(true)
if (setTopicDispatch) {
dispatch(batchActions([selectTreeTopicDispatch, setTopicDispatch]))
} else {
dispatch(selectTreeTopicDispatch)
}
}, 70)
const selectTreeTopicDispatch = {
selectedTopic: topic,
type: ActionTypes.TREE_SELECT_TOPIC,
}
export const showTree = (tree?: q.Tree<TopicViewModel>) => (dispatch: Dispatch<any>, getState: () => AppState): AnyAction => {
const visibleTree = getState().tree.tree
const connectionTree = getState().connection.tree
dispatch({
type: SidebarActionTypes.SIDEBAR_SET_COMPARE_MESSAGE,
message: undefined,
})
if (setTopicDispatch) {
dispatch(batchActions([selectTreeTopicDispatch, setTopicDispatch]))
} else {
dispatch(selectTreeTopicDispatch)
}
},
70
)
function destroyUnreferencedTree(state: AppState) {
const visibleTree = state.tree.get('tree')
const connectionTree = state.connection.tree
// Stop updates of old tree
if (visibleTree !== connectionTree && visibleTree) {
if (visibleTree && visibleTree !== connectionTree) {
console.warn('destroy')
visibleTree.stopUpdating()
visibleTree.destroy()
}
}
export const resetStore =
() =>
(dispatch: Dispatch<any>, getState: () => AppState): AnyAction => {
destroyUnreferencedTree(getState())
return dispatch({
type: ActionTypes.TREE_RESET_STORE,
})
}
return dispatch({
tree,
type: ActionTypes.TREE_SHOW_TREE,
export const showTree =
(tree: q.Tree<TopicViewModel> | undefined) =>
(dispatch: Dispatch<any>, getState: () => AppState): AnyAction => {
destroyUnreferencedTree(getState())
return dispatch({
tree,
type: ActionTypes.TREE_SHOW_TREE,
})
}
export const togglePause = (tree?: q.Tree<TopicViewModel>) => (dispatch: Dispatch<any>, getState: () => AppState) => {
const paused = getState().tree.get('paused')
const tree = getState().tree.get('tree')
const changes = tree ? tree.unmergedChanges().length : 0
if (tree) {
paused ? tree.resume() : tree.pause()
}
if (paused && changes > 0) {
dispatch(globalActions.showNotification('Applying recorded changes.'))
}
// Allow for notification to be displayed
setTimeout(() => {
if (paused && changes > 0) {
dispatch(globalActions.showNotification(`Successfully applied ${changes} changes.`))
}
}, 50)
dispatch({
type: paused ? ActionTypes.TREE_RESUME_UPDATES : ActionTypes.TREE_PAUSE_UPDATES,
})
}
+3 -3
View File
@@ -1,13 +1,13 @@
import { ActionTypes, CustomAction } from '../reducers'
import { ActionTypes, GlobalAction } from '../reducers/Global'
export const showUpdateNotification = (show: boolean): CustomAction => {
export const showUpdateNotification = (show: boolean): GlobalAction => {
return {
type: ActionTypes.showUpdateNotification,
showUpdateNotification: show,
}
}
export const showUpdateDetails = (show: boolean): CustomAction => {
export const showUpdateDetails = (show: boolean): GlobalAction => {
return {
type: ActionTypes.showUpdateDetails,
showUpdateDetails: show,
+54
View File
@@ -0,0 +1,54 @@
import * as q from '../../../backend/src/Model'
import { AppState } from '../reducers'
import { Dispatch } from 'redux'
import { makePublishEvent, rendererEvents } from '../../../events'
import { moveSelectionUpOrDownwards } from './visibleTreeTraversal'
import { globalActions } from '.'
export const clearTopic =
(topic: q.TreeNode<any>, recursive: boolean) => async (dispatch: Dispatch<any>, getState: () => AppState) => {
const topicsForPurging = recursive ? [topic, ...topic.childTopics()] : [topic]
if (recursive) {
const topicCount = topic.childTopicCount()
const topicDelta = topic.hasMessage() ? -1 : 0
const childTopicsMessage =
topicCount + topicDelta > 0
? ` and ${topicCount + topicDelta} child ${topicCount + topicDelta === 1 ? 'topic' : 'topics'}`
: ''
const confirmed = await dispatch(
globalActions.requestConfirmation(
'Confirm delete',
`Do you want to clear "${topic.path()}"${childTopicsMessage}?\n\nThis function will send an empty payload (QoS 0, retain) to this and every subtopic, clearing retained topics in the process. Only use this function if you know what you are doing.`
)
)
if (!confirmed) {
return
}
}
dispatch(moveSelectionUpOrDownwards('next'))
const { connectionId } = getState().connection
if (!connectionId) {
return
}
const publishEvent = makePublishEvent(connectionId)
topicsForPurging
.filter(t => t.path() !== '' && t.hasMessage())
.map(t => t.path())
.forEach((path, idx) => {
const mqttMessage = {
topic: path,
payload: null,
retain: true,
qos: 0 as 0,
messageId: undefined,
}
// Rate limit deletion
setTimeout(() => rendererEvents.emit(publishEvent, mqttMessage), 20 * idx)
})
}
+17 -4
View File
@@ -1,8 +1,21 @@
import * as settingsActions from './Settings'
import * as chartActions from './Charts'
import * as connectionActions from './Connection'
import * as connectionManagerActions from './ConnectionManager'
import * as globalActions from './Global'
import * as publishActions from './Publish'
import * as settingsActions from './Settings'
import * as sidebarActions from './Sidebar'
import * as treeActions from './Tree'
import * as updateNotifierActions from './UpdateNotifier'
import * as connectionActions from './Connection'
import * as sidebarActons from './Sidebar'
export { settingsActions, treeActions, publishActions, updateNotifierActions, connectionActions, sidebarActons }
export {
settingsActions,
treeActions,
chartActions,
publishActions,
updateNotifierActions,
connectionActions,
sidebarActions,
connectionManagerActions,
globalActions,
}
+94
View File
@@ -0,0 +1,94 @@
import { ConfigMigrator, Migration } from '../../utils/ConfigMigrator'
import { ConnectionDictionary } from '../ConnectionManager'
import { ConnectionOptions } from '../../model/ConnectionOptions'
export interface ConnectionOptionsV0 {
type: 'mqtt'
id: string
host: string
protocol: 'mqtt' | 'ws'
basePath?: string
port: number
name: string
username?: string
password?: string
encryption: boolean
certValidation: boolean
// selfSignedCertificate?: CertificateParameters
// clientCertificate?: CertificateParameters
// clientKey?: CertificateParameters
clientId?: string
subscriptions: Array<string>
}
let migrations: Migration[] = [
// iot.eclipse.org ha moved to mqtt.eclipse.org
{
from: undefined,
apply: (connection: ConnectionOptionsV0): ConnectionOptionsV0 => {
if (connection.id == 'iot.eclipse.org' && connection.host == 'iot.eclipse.org' && connection.port == 1883) {
return {
...connection,
id: 'mqtt.eclipse.org',
host: 'mqtt.eclipse.org',
name: 'mqtt.eclipse.org',
}
}
return {
...connection,
}
},
},
// Remove stored clientId if it is the default generated client id. This allows to connect multiple instances of mqtt explorer to the same broker.
// A randomly generated clientId will be used if no clientId is set.
{
from: undefined,
apply: (connection: ConnectionOptionsV0): ConnectionOptionsV0 => {
if (connection.clientId && /mqtt-explorer-[0-9a-f]{8}/.test(connection.clientId)) {
return {
...connection,
clientId: undefined,
}
}
return {
...connection,
}
},
},
// Added QoS level to subscription options
{
from: undefined,
apply: (connection: ConnectionOptionsV0): ConnectionOptions => {
return {
...connection,
configVersion: 1,
subscriptions: connection.subscriptions.map(topic => ({ topic, qos: 0 })),
}
},
},
]
const connectionMigrator = new ConfigMigrator(migrations)
function isMigrationNecessary(connections: ConnectionDictionary): boolean {
return Object.values(connections)
.map(connection => connectionMigrator.isMigrationNecessary(connection))
.reduce((a, b) => a || b, false)
}
function applyMigrations(connections: ConnectionDictionary): ConnectionDictionary {
let newConnectionDictionary: ConnectionDictionary = {}
Object.keys(connections).forEach(key => {
let newConnection = connectionMigrator.applyMigrations(connections[key]) as any
newConnectionDictionary[newConnection.id] = newConnection
})
return newConnectionDictionary
}
export const connectionsMigrator = {
isMigrationNecessary,
applyMigrations,
}
+138
View File
@@ -0,0 +1,138 @@
import * as q from '../../../backend/src/Model'
import { AppState } from '../reducers'
import { Dispatch } from 'redux'
import { selectTopic } from './Tree'
import { SettingsState } from '../reducers/Settings'
import { sortedNodes } from '../sortedNodes'
import { TopicViewModel } from '../model/TopicViewModel'
export const moveSelectionUpOrDownwards =
(direction: 'next' | 'previous') =>
(dispatch: Dispatch<any>, getState: () => AppState): any => {
const state = getState()
const selected = state.tree.get('selectedTopic')
const tree = state.tree.get('tree')
if (!selected || !tree) {
if (tree) {
dispatch(selectTopic(tree))
}
return
}
const nextTreeNode = nextVisibleElementInTree(state.settings, tree, selected, direction)
if (nextTreeNode && nextTreeNode.viewModel) {
dispatch(selectTopic(nextTreeNode))
}
}
export const moveInward =
() =>
(dispatch: Dispatch<any>, getState: () => AppState): any => {
const state = getState()
const selected = state.tree.get('selectedTopic')
if (!selected || !selected.viewModel) {
return
}
if (!selected.viewModel.isExpanded() && selected.edgeCount() > 0) {
selected.viewModel.setExpanded(true, true)
} else {
dispatch(moveSelectionUpOrDownwards('next'))
}
}
export const moveOutward =
() =>
(dispatch: Dispatch<any>, getState: () => AppState): any => {
const state = getState()
const selected = state.tree.get('selectedTopic')
if (!selected || !selected.viewModel) {
return
}
if (selected.viewModel.isExpanded() && selected.edgeCount() > 0) {
selected.viewModel.setExpanded(false, true)
} else {
dispatch(moveSelectionUpOrDownwards('previous'))
}
}
function isTreeNodeVisible(treeNode: q.TreeNode<any>) {
return Boolean(treeNode.viewModel)
}
function nextVisibleElementInTree(
settings: SettingsState,
tree: q.Tree<TopicViewModel>,
node: q.TreeNode<TopicViewModel>,
direction: 'next' | 'previous'
): q.TreeNode<TopicViewModel> | undefined {
if (direction === 'next') {
return findNextNodeDownward(settings, node)
} else {
return findNextNodeUpward(settings, node)
}
}
/** Not very efficient but easy to implement, complexity should not be an issue here */
function findNextNodeUpward(
settings: SettingsState,
treeNode: q.TreeNode<TopicViewModel>
): q.TreeNode<TopicViewModel> | undefined {
const parent = treeNode.sourceEdge && treeNode.sourceEdge.source
if (!parent) {
return undefined
}
const neighborNodes = sortedNodes(settings, parent)
const nodeIdx = neighborNodes.findIndex(n => n.path() === treeNode.path())
if (nodeIdx === 0) {
return parent
}
const upwardNeighbor = neighborNodes[nodeIdx - 1]
if (upwardNeighbor) {
return lastVisibleChild(settings, upwardNeighbor)
} else {
return findNextNodeUpward(settings, parent)
}
}
function lastVisibleChild(settings: SettingsState, treeNode: q.TreeNode<TopicViewModel>): q.TreeNode<TopicViewModel> {
const nodes = sortedNodes(settings, treeNode).filter(isTreeNodeVisible)
if (nodes.length === 0) {
return treeNode
}
return lastVisibleChild(settings, nodes[nodes.length - 1])
}
function findNextNodeDownward(
settings: SettingsState,
treeNode: q.TreeNode<TopicViewModel>
): q.TreeNode<TopicViewModel> | undefined {
const children = sortedNodes(settings, treeNode).filter(isTreeNodeVisible)
const firstChild = children[0]
if (firstChild) {
return firstChild
}
return findNextNodeDownwardNeighbor(settings, treeNode)
}
function findNextNodeDownwardNeighbor(
settings: SettingsState,
treeNode: q.TreeNode<TopicViewModel>
): q.TreeNode<TopicViewModel> | undefined {
const parent = treeNode.sourceEdge && treeNode.sourceEdge.source
if (!parent) {
return undefined
}
const neighborNodes = sortedNodes(settings, parent).filter(isTreeNodeVisible)
const nodeIdx = neighborNodes.findIndex(n => n.path() === treeNode.path())
const downwardNeighbor = neighborNodes[nodeIdx + 1]
if (downwardNeighbor) {
return downwardNeighbor
} else {
return findNextNodeDownwardNeighbor(settings, parent)
}
}
+127
View File
@@ -0,0 +1,127 @@
// 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' }
}))
}
})
// Listen for auth-status from server (sent on connection)
socket.on('auth-status', (data: { authDisabled: boolean }) => {
console.log('Auth status received from server:', data)
// Dispatch custom event with auth status
if (typeof window !== 'undefined') {
window.dispatchEvent(new CustomEvent('mqtt-auth-status', {
detail: { authDisabled: data.authDisabled }
}))
}
})
/**
* 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'
-4
View File
@@ -1,4 +0,0 @@
import { electronRendererTelementry } from 'electron-telemetry'
const spareMeFromGc = electronRendererTelementry
electronRendererTelementry.registerErrorHandler()
+164
View File
@@ -0,0 +1,164 @@
import ConfirmationDialog from './ConfirmationDialog'
import ConnectionSetup from './ConnectionSetup/ConnectionSetup'
import CssBaseline from '@mui/material/CssBaseline'
import ErrorBoundary from './ErrorBoundary'
import Notification from './Layout/Notification'
import React from 'react'
import TitleBar from './Layout/TitleBar'
import UpdateNotifier from './UpdateNotifier'
import { AppState } from '../reducers'
import { bindActionCreators } from 'redux'
import { ConfirmationRequest } from '../reducers/Global'
import { connect } from 'react-redux'
import { globalActions, settingsActions } from '../actions'
import { Theme } from '@mui/material/styles'
import { withStyles } from '@mui/styles'
;(window as any).global = window
const Settings = React.lazy(() => import('./SettingsDrawer/Settings'))
const ContentView = React.lazy(() => import('./Layout/ContentView'))
interface Props {
connectionId: string
classes: any
settingsVisible: boolean
error?: string
notification?: string
actions: typeof globalActions
settingsActions: typeof settingsActions
launching: boolean
confirmationRequests: Array<ConfirmationRequest>
}
class App extends React.PureComponent<Props, {}> {
constructor(props: any) {
super(props)
this.state = {}
}
private renderNotification() {
const message = this.props.error || this.props.notification
const isError = message === this.props.error
if (message) {
// Guard in case someone ever calls showError with an error instead of a string
const str = typeof message === 'string' ? message : JSON.stringify(message)
return (
<Notification
message={str}
type={isError ? 'error' : 'notification'}
onClose={() => {
isError ? this.props.actions.showError(undefined) : this.props.actions.showNotification(undefined)
}}
/>
)
}
return null
}
public componentDidMount() {
this.props.settingsActions.loadSettings()
}
public render() {
const { settingsVisible } = this.props
const { content, contentShift, centerContent, paneDefaults, heightProperty } = this.props.classes
if (this.props.launching) {
return null
}
const anyProps: any = {}
return (
<div className={centerContent}>
<CssBaseline />
<ErrorBoundary>
<ConfirmationDialog confirmationRequests={this.props.confirmationRequests} />
{this.renderNotification()}
<React.Suspense fallback={<div></div>}>
<Settings {...anyProps} />
</React.Suspense>
<div className={centerContent}>
<div className={`${settingsVisible ? contentShift : content}`}>
<TitleBar />
</div>
<div className={settingsVisible ? contentShift : content}>
<React.Suspense fallback={<div></div>}>
<ContentView
heightProperty={heightProperty}
connectionId={this.props.connectionId}
paneDefaults={paneDefaults}
/>
</React.Suspense>
</div>
</div>
<UpdateNotifier />
<ConnectionSetup />
</ErrorBoundary>
</div>
)
}
}
const styles = (theme: Theme) => {
const drawerWidth = 300
const contentBaseStyle = {
width: '100vw',
backgroundColor: theme.palette.background.default,
}
return {
heightProperty: {
height: '100%', // 'calc(100vh - 64px) !important',
},
paneDefaults: {
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
display: 'block' as 'block',
height: 'calc(100vh - 64px)',
},
centerContent: {
width: '100vw',
overflow: 'hidden' as 'hidden',
},
content: {
...contentBaseStyle,
transition: theme.transitions.create('transform', {
easing: theme.transitions.easing.sharp,
duration: theme.transitions.duration.leavingScreen,
}),
transform: 'translateX(0px)',
},
contentShift: {
...contentBaseStyle,
backgroundColor: theme.palette.background.default,
transition: theme.transitions.create('transform', {
easing: theme.transitions.easing.easeOut,
duration: theme.transitions.duration.enteringScreen,
}),
transform: `translateX(${drawerWidth}px)`,
},
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(globalActions, dispatch),
settingsActions: bindActionCreators(settingsActions, dispatch),
}
}
const mapStateToProps = (state: AppState) => {
return {
settingsVisible: state.globalState.get('settingsVisible'),
connectionId: state.connection.connectionId,
error: state.globalState.get('error'),
notification: state.globalState.get('notification'),
highlightTopicUpdates: state.settings.get('highlightTopicUpdates'),
launching: state.globalState.get('launching'),
confirmationRequests: state.globalState.get('confirmationRequests'),
}
}
export default withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(App))
-132
View File
@@ -1,132 +0,0 @@
import * as React from 'react'
import * as q from '../../../backend/src/Model'
import { AppState } from '../reducers'
import { Typography } from '@material-ui/core'
import { StyleRulesCallback, withStyles } from '@material-ui/core/styles'
import { connect } from 'react-redux'
import { TopicViewModel } from '../TopicViewModel'
const abbreviate = require('number-abbreviate')
interface Stats {
topic: string
title: string
}
const styles: StyleRulesCallback = theme => ({
flex: {
display: 'flex',
width: '100%',
},
container: {
width: '100%',
height: '224px',
backgroundColor: 'rebeccapurple',
marginBottom: 0,
marginTop: 'auto',
padding: '8px',
},
})
interface Props {
classes: any
tree?: q.Tree<TopicViewModel>
}
class BrokerStatistics extends React.Component<Props, {}> {
constructor(props: any) {
super(props)
this.state = {}
}
public render() {
const { tree, classes } = this.props
if (!tree || !tree.findNode('$SYS')) {
return null
}
const stats: any = {
broker: {
topic: '$SYS/broker/version',
title: 'Broker',
},
clients: {
topic: '$SYS/broker/clients/total',
title: 'Clients',
},
subscriptions: {
topic: '$SYS/broker/subscriptions/count',
title: 'Subscriptions',
},
received: {
topic: '$SYS/broker/messages/received',
title: 'Received',
},
sent: {
topic: '$SYS/broker/messages/sent',
title: 'Sent',
},
received5m: {
topic: '$SYS/broker/load/messages/received/5min',
title: 'Received last 5min',
},
sent5m: {
topic: '$SYS/broker/load/messages/sent/5min',
title: 'Sent 5m',
},
heap: {
topic: '$SYS/broker/heap/current',
title: 'Memory',
},
heapMax: {
topic: '$SYS/broker/heap/maximum',
title: 'Memory (max)',
},
}
return (
<div className={classes.container}>
{this.renderStat(tree, stats.broker)}
{this.renderPair(tree, stats.sent, stats.received)}
{this.renderPair(tree, stats.clients, stats.subscriptions)}
{this.renderPair(tree, stats.sent5m, stats.received5m)}
{this.renderPair(tree, stats.heap, stats.heapMax)}
</div>
)
}
private renderPair(tree: q.Tree<TopicViewModel>, a: Stats, b: Stats) {
return (
<div className={this.props.classes.flex}>
<div style={{ flex: 1 }}>{this.renderStat(tree, a)}</div>
<div style={{ flex: 1 }}>{this.renderStat(tree, b)}</div>
</div>
)
}
public renderStat(tree: q.Tree<TopicViewModel>, stat: Stats) {
const node = tree.findNode(stat.topic)
if (!node) {
return null
}
let value = node.message && node.message.value
value = !isNaN(value) ? abbreviate(value) : value
return (
<div key={stat.title}>
<Typography><b>{stat.title}</b></Typography>
<Typography style={{ paddingLeft: '8px' }}><i>{value}</i></Typography>
</div>
)
}
}
const mapStateToProps = (state: AppState) => {
return {
tree: state.connection.tree,
}
}
export default withStyles(styles)(connect(mapStateToProps)(BrokerStatistics))
+146
View File
@@ -0,0 +1,146 @@
import * as React from 'react'
import { LoginDialog } from './LoginDialog'
import { updateSocketAuth, connectSocket } from '../browserEventBus'
import { isBrowserMode } from '../utils/browserMode'
import { AuthContext } from '../contexts/AuthContext'
interface BrowserAuthWrapperProps {
children: React.ReactNode
}
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 [waitTimeSeconds, setWaitTimeSeconds] = React.useState<number | undefined>()
const [isConnecting, setIsConnecting] = React.useState(false)
const [authCheckComplete, setAuthCheckComplete] = React.useState(false)
const [authDisabled, setAuthDisabled] = React.useState(false)
React.useEffect(() => {
if (!isBrowserMode) {
// Not in browser mode, skip authentication
setIsAuthenticated(true)
setAuthCheckComplete(true)
return
}
// Listen for auth status from socket connection
const handleAuthStatus = (event: CustomEvent) => {
const { authDisabled } = event.detail
setAuthDisabled(authDisabled)
if (authDisabled) {
// Authentication is disabled on server
console.log('Authentication is disabled on server, skipping login')
setIsAuthenticated(true)
setShowLogin(false)
setAuthCheckComplete(true)
} else {
// Authentication is enabled, check if we have credentials
setAuthCheckComplete(true)
const username = sessionStorage.getItem('mqtt-explorer-username')
const password = sessionStorage.getItem('mqtt-explorer-password')
if (username && password) {
// Credentials exist, connection will authenticate automatically
setIsConnecting(true)
} else {
// No credentials, show login dialog
setShowLogin(true)
}
}
}
// 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)
}
}
// Connect socket to trigger auth-status event
connectSocket()
window.addEventListener('mqtt-auth-status', handleAuthStatus as EventListener)
window.addEventListener('mqtt-auth-success', handleAuthSuccess as EventListener)
window.addEventListener('mqtt-auth-error', handleAuthError as EventListener)
return () => {
window.removeEventListener('mqtt-auth-status', handleAuthStatus as EventListener)
window.removeEventListener('mqtt-auth-success', handleAuthSuccess as EventListener)
window.removeEventListener('mqtt-auth-error', handleAuthError as EventListener)
}
}, [])
const handleLogin = (username: string, password: string) => {
try {
// Clear any previous error
setLoginError(undefined)
setWaitTimeSeconds(undefined)
setIsConnecting(true)
// Update socket auth and reconnect (no page reload needed)
updateSocketAuth(username, password)
} catch (error) {
console.error('Failed to update socket auth:', error)
setLoginError('Failed to connect. Please try again.')
setIsConnecting(false)
}
}
if (!isBrowserMode) {
// Not in browser mode, render children directly
return <>{props.children}</>
}
// Show nothing while checking auth status to avoid flash
if (!authCheckComplete) {
return null
}
if (!isAuthenticated) {
return <LoginDialog open={showLogin} onLogin={handleLogin} error={loginError} waitTimeSeconds={waitTimeSeconds} />
}
return <AuthContext.Provider value={{ authDisabled }}>{props.children}</AuthContext.Provider>
}
+101
View File
@@ -0,0 +1,101 @@
import '../../react-vis-compat' // React 19 compatibility shim for react-vis
import DateFormatter from '../helper/DateFormatter'
import NoData from './NoData'
import NumberFormatter from '../helper/NumberFormatter'
import React, { memo, useCallback, useRef, useEffect } from 'react'
import TooltipComponent from './TooltipComponent'
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 { useCustomXDomain } from './effects/useCustomXDomain'
import { useCustomYDomain } from './effects/useCustomYDomain'
import 'react-vis/dist/style.css'
const { XYPlot, LineMarkSeries, YAxis, HorizontalGridLines, Hint } = require('react-vis')
const abbreviate = require('number-abbreviate')
export interface Props {
data: Array<{ x: number; y: number }>
interpolation?: PlotCurveTypes
range?: [number?, number?]
timeRangeStart?: number
color?: string
}
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 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 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 formatYAxis = useCallback((num: number) => abbreviate(num), [])
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 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>
)
})
+15
View File
@@ -0,0 +1,15 @@
export interface Point {
x: number
y: number
}
export interface Tooltip {
value: Array<TooltipRows>
point: Point
element: Element | null
}
export interface TooltipRows {
title: React.ReactElement
value: React.ReactElement
}
+27
View File
@@ -0,0 +1,27 @@
import React, { memo } from 'react'
import { Typography } from '@mui/material'
function NoData() {
return (
<div
style={{
height: '100%',
position: 'absolute',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
color: '#ccc',
verticalAlign: 'middle',
paddingLeft: '30px',
zIndex: 10,
}}
>
<Typography style={{ fontWeight: 'bold' }} variant="h5">
No Data
</Typography>
</div>
)
}
export default memo(NoData)
@@ -0,0 +1,60 @@
import React, { memo } from 'react'
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 }) {
const theme = useTheme()
const { tooltip } = props
return (
<Popper
style={Boolean(tooltip) ? { transition: 'all 0.1s ease-out' } : undefined}
open={Boolean(tooltip)}
transition={true}
placement="top"
anchorEl={tooltip && tooltip.element}
>
<div
style={{
paddingBottom: '8px',
transition: 'all 0.5s ease',
}}
>
<Fade in={Boolean(tooltip)} timeout={300}>
<Grow in={Boolean(tooltip)} timeout={300}>
<Paper
style={{
padding: '4px',
marginTop: '-12px',
backgroundColor: fade(
theme.palette.mode === 'light'
? theme.palette.background.paper
: theme.palette.background.default,
0.7
),
}}
>
<table style={{ lineHeight: '1.25em' }}>
<tbody>
{tooltip &&
tooltip.value.map((v: any, idx: number) => (
<tr key={idx}>
<td>
<Typography style={{ lineHeight: '1.2' }}>{v.title}</Typography>
</td>
<td>
<Typography style={{ lineHeight: '1.2' }}>{v.value}</Typography>
</td>
</tr>
))}
</tbody>
</table>
</Paper>
</Grow>
</Fade>
</div>
</Popper>
)
}
export default memo(TooltipComponent)
@@ -0,0 +1,10 @@
import { useMemo } from 'react'
import { Props } from '../Chart'
export function useCustomXDomain(props: Props): [number, number] | undefined {
return useMemo(() => {
const lastDataPoint = [...props.data].sort((a, b) => b.x - a.x)[0]
const lastDataDate = lastDataPoint ? lastDataPoint.x : Date.now()
return props.timeRangeStart ? [Date.now() - props.timeRangeStart, lastDataDate] : undefined
}, [props.data, props.timeRangeStart])
}
@@ -0,0 +1,45 @@
import { Props } from '../Chart'
import { useMemo } from 'react'
import { Point } from '../Model'
function defaultFor(a: number | undefined, b: number) {
return a === undefined ? b : a
}
export function useCustomYDomain(props: Props) {
return useMemo(() => {
const data = props.data
const calculatedDomain = domainForData(data)
const yDomain: [number, number] = props.range
? [defaultFor(props.range[0], calculatedDomain[0]), defaultFor(props.range[1], calculatedDomain[1])]
: calculatedDomain
return yDomain
}, [props.data, props.range])
}
function domainForData(data: Array<Point>): [number, number] {
if (!data[0]) {
const defaultDomain: [number, number] = [-1, 1]
return defaultDomain
}
let max = data[0].y
let min = data[0].y
data.forEach(d => {
if (max < d.y) {
max = d.y
}
if (min > d.y) {
min = d.y
}
})
if ((max === 1 || max === 0) && (min === 1 || min === 0)) {
return [0, 1]
}
if (min === max) {
return [min - 0.5 * min, min + 0.5 * min]
}
return [min, max]
}
+17
View File
@@ -0,0 +1,17 @@
import { PlotCurveTypes } from '../../reducers/Charts'
export function mapCurveType(type: PlotCurveTypes | undefined) {
switch (type) {
case 'curve':
return 'curveMonotoneX'
case 'linear':
return 'curveLinear'
case 'cubic_basis_spline':
return 'curveBasis'
case 'step_after':
return 'curveStepAfter'
case 'step_before':
return 'curveStepBefore'
default:
return 'curveMonotoneX'
}
}
@@ -0,0 +1,34 @@
import React, { useRef } from 'react'
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'
export function ChartActions(props: {
paused: boolean
togglePause: () => void
parameters: ChartParameters
onRemove: () => void
resetDataAction: () => void
}) {
const menuAnchor = useRef()
return (
<div style={{ display: 'flex' }}>
<CustomIconButton tooltip={props.paused ? 'Resume chart' : 'Pause chart'} onClick={props.togglePause}>
{props.paused ? <Play /> : <Pause />}
</CustomIconButton>
<SettingsButton menuAnchor={menuAnchor} parameters={props.parameters} resetDataAction={props.resetDataAction} />
<CustomIconButton tooltip="Remove chart" onClick={props.onRemove}>
<Clear data-test-type="RemoveChart" data-test={`${props.parameters.topic}-${props.parameters.dotPath || ''}`} />
</CustomIconButton>
<div style={{ width: 0, overflow: 'hidden' }}>
{/* Helper element to provide an anchor element for the menu,
* so the menu prefers not to overlap with the chart
*/}
<div style={{ marginLeft: '11px' }} ref={menuAnchor as any} />
</div>
</div>
)
}
@@ -0,0 +1,68 @@
import React, { memo } from 'react'
import { bindActionCreators } from 'redux'
import { chartActions } from '../../../actions'
import { ChartParameters } from '../../../reducers/Charts'
import { connect } from 'react-redux'
import { Menu, MenuItem } from '@mui/material'
import { colors as createColors } from './colors'
function chartParametersForColor(chart: ChartParameters, color?: string) {
return {
color,
topic: chart.topic,
dotPath: chart.dotPath,
}
}
const colors: Array<string> = createColors()
function ColorSettings(props: {
chart: ChartParameters
actions: {
chart: typeof chartActions
}
anchorEl?: Element
open: boolean
close: () => void
}) {
const setColor = React.useCallback(
(color?: string) => props.actions.chart.updateChart(chartParametersForColor(props.chart, color)),
[props.chart]
)
const menuItems = React.useMemo(() => {
return colors.map(color => (
<MenuItem
style={{ minWidth: '8em', minHeight: '36px', backgroundColor: color, textAlign: 'center' }}
key={color}
onClick={() => setColor(color)}
>
{props.chart.color === color ? 'X' : ''}
</MenuItem>
))
}, [colors, props.chart])
return (
<Menu anchorEl={props.anchorEl} open={props.open} onClose={props.close}>
<MenuItem
style={{ minWidth: '8em', minHeight: '36px', textAlign: 'center' }}
key="none"
onClick={() => setColor()}
selected={props.chart.color === undefined}
>
default
</MenuItem>
{menuItems}
</Menu>
)
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
chart: bindActionCreators(chartActions, dispatch),
},
}
}
export default connect(undefined, mapDispatchToProps)(memo(ColorSettings))
@@ -0,0 +1,68 @@
import * as React from 'react'
import { AppState } from '../../../reducers'
import { bindActionCreators } from 'redux'
import { chartActions } from '../../../actions'
import { ChartParameters, PlotCurveTypes } from '../../../reducers/Charts'
import { connect } from 'react-redux'
import { Menu, MenuItem, Typography } from '@mui/material'
function chartParametersForAction(chart: ChartParameters, action: string) {
return {
topic: chart.topic,
dotPath: chart.dotPath,
interpolation: action as any,
}
}
const curves: Array<PlotCurveTypes> = ['curve', 'linear', 'step_after', 'step_before', 'cubic_basis_spline']
function InterpolationSettings(props: {
chart: ChartParameters
actions: {
chart: typeof chartActions
}
anchorEl?: Element
open: boolean
close: () => void
}) {
const callbacks = React.useMemo(() => {
const createCurveCallback = (curve: PlotCurveTypes) => () => {
props.actions.chart.updateChart(chartParametersForAction(props.chart, curve))
}
const callbacks: { [key: string]: () => void } = {}
for (const curve of curves) {
callbacks[curve] = createCurveCallback(curve)
}
return callbacks
}, [curves])
const menuItems = React.useMemo(() => {
return curves.map(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>
))
}, [curves, props.chart])
return (
<Menu anchorEl={props.anchorEl} open={props.open} onClose={props.close}>
{menuItems}
</Menu>
)
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
chart: bindActionCreators(chartActions, dispatch),
},
}
}
export default connect(undefined, mapDispatchToProps)(InterpolationSettings)
@@ -0,0 +1,36 @@
import * as React from 'react'
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 '@mui/material'
function MoveUp(props: { actions: { chart: typeof chartActions }; chart: ChartParameters; close: () => void }) {
const moveUp = React.useCallback(() => {
props.actions.chart.moveChartUp({
topic: props.chart.topic,
dotPath: props.chart.dotPath,
})
props.close()
}, [props.chart])
return (
<MenuItem key="size" onClick={moveUp}>
<ListItemIcon>
<ArrowUpward />
</ListItemIcon>
<Typography variant="inherit">Move up</Typography>
</MenuItem>
)
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
chart: bindActionCreators(chartActions, dispatch),
},
}
}
export default connect(undefined, mapDispatchToProps)(MoveUp)
@@ -0,0 +1,117 @@
import React, { useCallback, useState, ChangeEvent, MouseEvent, useRef, useEffect, useMemo } from 'react'
import { ChartParameters } from '../../../reducers/Charts'
import { Menu, TextField, Typography } from '@mui/material'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { chartActions } from '../../../actions'
import { KeyCodes } from '../../../utils/KeyCodes'
interface Props {
actions: { chart: typeof chartActions }
chart: ChartParameters
anchorEl?: Element
open: boolean
onClose: () => void
}
function RangeSettings(props: Props) {
const dismissClick = useCallback((e: MouseEvent) => e.stopPropagation(), [])
const [rangeFrom, setRangeFrom] = useState<string | number | undefined>(props.chart.range && props.chart.range.from)
const [rangeTo, setRangeTo] = useState<string | number | undefined>(props.chart.range && props.chart.range.to)
useRangeStateToFireUpdateAction(rangeFrom, rangeTo, props)
const rangeFromRef = useRef<HTMLInputElement>()
const rangeToRef = useRef<HTMLInputElement>()
useEffect(() => {
rangeFromRef.current && rangeFromRef.current.focus()
}, [props.open])
const handleKeyEvents = (e: React.KeyboardEvent<any>) => {
if (e.keyCode === KeyCodes.tab) {
// Switch focus between those two
if (document.activeElement === rangeFromRef.current) {
rangeToRef.current && rangeToRef.current.focus()
} else {
rangeFromRef.current && rangeFromRef.current.focus()
}
// Prevent closing the menu
e.stopPropagation()
// Prevent default tab behavior (focus/blur)
e.preventDefault()
} else if (e.keyCode === KeyCodes.enter) {
props.onClose()
}
}
const setFromHandler = useCallback((e: ChangeEvent<HTMLInputElement>) => setRangeFrom(e.target.value), [])
const setToHandler = useCallback((e: ChangeEvent<HTMLInputElement>) => setRangeTo(e.target.value), [])
return useMemo(
() => (
<Menu
style={{ textAlign: 'center' }}
keepMounted={true}
anchorEl={props.anchorEl}
open={props.open}
onClose={props.onClose}
onKeyDownCapture={handleKeyEvents}
>
<div style={{ padding: '0 16px', width: '275px' }}>
<Typography>Define custom ranges for the Y-Axis</Typography>
<TextField
inputProps={{
ref: rangeFromRef,
}}
autoFocus={true}
style={{ marginTop: '0' }}
label="from"
value={rangeFrom}
onChange={setFromHandler}
margin="normal"
/>
<TextField
inputProps={{
ref: rangeToRef,
}}
style={{ marginLeft: '8px', marginTop: '0' }}
onClick={dismissClick}
label="to"
value={rangeTo}
onChange={setToHandler}
margin="normal"
/>
</div>
</Menu>
),
[rangeFrom, rangeTo, props.open]
)
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
chart: bindActionCreators(chartActions, dispatch),
},
}
}
export default connect(undefined, mapDispatchToProps)(RangeSettings)
function useRangeStateToFireUpdateAction(
rangeFrom: string | number | undefined,
rangeTo: string | number | undefined,
props: Props
) {
React.useEffect(() => {
const from = parseFloat(rangeFrom as any)
const to = parseFloat(rangeTo as any)
props.actions.chart.updateChart({
topic: props.chart.topic,
dotPath: props.chart.dotPath,
range: {
from: isNaN(from) ? undefined : from,
to: isNaN(to) ? undefined : to,
},
})
}, [rangeFrom, rangeTo])
}
@@ -0,0 +1,38 @@
import * as React from 'react'
import ChartSettings from '.'
import CustomIconButton from '../../helper/CustomIconButton'
import MoreVertIcon from '@mui/icons-material/Settings'
import { ChartParameters } from '../../../reducers/Charts'
export function SettingsButton(props: {
parameters: ChartParameters
resetDataAction: () => void
menuAnchor: React.MutableRefObject<undefined>
}) {
const [visible, setVisible] = React.useState(false)
const toggleSettings = React.useCallback(() => {
setVisible(!visible)
}, [visible])
const close = React.useCallback(() => {
setVisible(false)
}, [])
return (
<span>
<ChartSettings
open={visible}
close={close}
anchorEl={props.menuAnchor}
chart={props.parameters}
resetDataAction={props.resetDataAction}
/>
<CustomIconButton tooltip="Chart settings" onClick={toggleSettings}>
<MoreVertIcon
data-test-type="ChartSettings"
data-test={`${props.parameters.topic}-${props.parameters.dotPath || ''}`}
/>
</CustomIconButton>
</span>
)
}
@@ -0,0 +1,50 @@
import React, { memo } from 'react'
import { ChartParameters } from '../../../reducers/Charts'
import { Menu, MenuItem, TextField, Typography } from '@mui/material'
import { connect } from 'react-redux'
import { bindActionCreators } from 'redux'
import { chartActions } from '../../../actions'
function Size(props: {
actions: { chart: typeof chartActions }
chart: ChartParameters
anchorEl?: Element
open: boolean
close: () => void
}) {
const setChartWidth = (width?: 'big' | 'medium' | 'small') => () => {
props.actions.chart.updateChart({
width,
topic: props.chart.topic,
dotPath: props.chart.dotPath,
})
props.close()
}
return (
<Menu anchorEl={props.anchorEl} open={props.open} onClose={props.close}>
<MenuItem selected={props.chart.width === undefined} onClick={setChartWidth()}>
<Typography variant="inherit">auto</Typography>
</MenuItem>
<MenuItem selected={props.chart.width === 'big'} onClick={setChartWidth('big')}>
<Typography variant="inherit">100% width</Typography>
</MenuItem>
<MenuItem selected={props.chart.width === 'medium'} onClick={setChartWidth('medium')}>
<Typography variant="inherit">50% width</Typography>
</MenuItem>
<MenuItem selected={props.chart.width === 'small'} onClick={setChartWidth('small')}>
<Typography variant="inherit">33% width</Typography>
</MenuItem>
</Menu>
)
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
chart: bindActionCreators(chartActions, dispatch),
},
}
}
export default connect(undefined, mapDispatchToProps)(memo(Size))
@@ -0,0 +1,99 @@
import React, { ChangeEvent, MouseEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { bindActionCreators } from 'redux'
import { Button, Menu, TextField, Typography } from '@mui/material'
import { chartActions } from '../../../actions'
import { ChartParameters } from '../../../reducers/Charts'
import { connect } from 'react-redux'
const parseDuration = require('parse-duration')
interface Props {
actions: { chart: typeof chartActions }
chart: ChartParameters
anchorEl?: Element
open: boolean
onClose: () => void
}
function TimeRangeSettings(props: Props) {
const dismissClick = useCallback((e: MouseEvent) => e.stopPropagation(), [])
const [value, setValue] = useState<string | undefined>(
props.chart.timeRange ? props.chart.timeRange.until : undefined
)
const ranges = ['all', '10s', '30s', '1m', '5m', '15m', '1h', '6h', '1d']
const manuallySetIntervalHandler = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value)
}, [])
useEffect(() => {
if (!value) {
props.actions.chart.updateChart({
...props.chart,
timeRange: undefined,
})
return
}
const canBeParsed = Boolean(parseDuration(value))
if (canBeParsed) {
props.actions.chart.updateChart({
...props.chart,
timeRange: {
until: value,
},
})
}
}, [value])
return useMemo(() => {
const createRangeHandler = (range: string) => (e: React.MouseEvent) => setValue(range === 'all' ? undefined : range)
return (
<Menu
style={{ textAlign: 'center' }}
keepMounted={true}
anchorEl={props.anchorEl}
open={props.open}
onClose={props.onClose}
>
<Typography>Chart data within a time interval</Typography>
<div style={{ padding: '0 16px', width: '275px', textAlign: 'center' }}>
{ranges.map(r => {
return (
<Button
style={{ margin: '4px', textTransform: 'none' }}
variant="contained"
key={r}
onClick={createRangeHandler(r)}
>
{r}
</Button>
)
})}
</div>
<Typography style={{ fontSize: '0.75em' }}>
<i>Limited to 500 data points</i>
</Typography>
<br />
<TextField
style={{ marginLeft: '8px', marginTop: '0' }}
onClick={dismissClick}
label="custom interval"
value={value || ''}
onChange={manuallySetIntervalHandler}
margin="normal"
/>
</Menu>
)
}, [value, props.open])
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
chart: bindActionCreators(chartActions, dispatch),
},
}
}
export default connect(undefined, mapDispatchToProps)(TimeRangeSettings)
@@ -0,0 +1,46 @@
import {
amber,
orange,
pink,
purple,
deepPurple,
teal,
red,
green,
lime,
indigo,
yellow,
brown,
blueGrey,
} from '@mui/material/colors'
export function colors() {
function colorToInt(color: string): [number, number, number] {
const str = color.replace('#', '')
return [parseInt(str.slice(0, 2), 16), parseInt(str.slice(2, 4), 16), parseInt(str.slice(4, 6), 16)]
}
function colorCompare(colorA: string, colorB: string) {
const a = colorToInt(colorA)
const b = colorToInt(colorB)
return Math.sqrt(Math.pow(a[0] - b[0], 2) + Math.pow(a[1] - b[1], 2) + Math.pow(a[2] - b[2], 2))
}
const colors: Array<string> = [
brown,
blueGrey,
amber,
orange,
pink,
purple,
deepPurple,
teal,
red,
green,
lime,
indigo,
yellow,
]
.map(color => [color[200], color[500], color[700]])
.reduce((a, b) => a.concat(b), [])
.sort((a, b) => colorCompare(a, b))
return colors
}
@@ -0,0 +1,125 @@
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 '@mui/icons-material/MultilineChart'
import RangeSettings from './RangeSettings'
import React, { memo } from 'react'
import Size from './Size'
import Sort from '@mui/icons-material/Sort'
import TimeRangeSettings from './TimeRangeSettings'
import { ChartParameters } from '../../../reducers/Charts'
import { Menu, MenuItem, ListItemIcon, Typography } from '@mui/material'
function ChartSettings(props: {
open: boolean
close: () => void
resetDataAction: () => void
chart: ChartParameters
anchorEl: React.MutableRefObject<undefined>
}) {
const [rangeVisible, setRangeVisible] = React.useState(false)
const [timeRangeVisible, setTimeRangeVisible] = React.useState(false)
const [interpolationVisible, setInterpolationVisible] = React.useState(false)
const [sizeVisible, setSizeVisible] = React.useState(false)
const [colorVisible, setColorVisible] = React.useState(false)
const open = props.open
const toggleRange = React.useCallback(() => {
if (open) {
props.close()
}
setRangeVisible(!rangeVisible)
}, [rangeVisible, open])
const toggleTimeRange = React.useCallback(() => {
if (open) {
props.close()
}
setTimeRangeVisible(!timeRangeVisible)
}, [timeRangeVisible, open])
const toggleInterpolation = React.useCallback(() => {
if (open) {
props.close()
}
setInterpolationVisible(!interpolationVisible)
}, [interpolationVisible, open])
const toggleSize = React.useCallback(() => {
if (open) {
props.close()
}
setSizeVisible(!sizeVisible)
}, [sizeVisible, open])
const toggleColor = React.useCallback(() => {
if (open) {
props.close()
}
setColorVisible(!colorVisible)
}, [colorVisible, open])
return (
<span>
<Menu id="long-menu" anchorEl={props.anchorEl.current} open={props.open} onClose={props.close}>
<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} data-menu-item="X-Axis range (Time)">
<ListItemIcon>
<BarChart />
</ListItemIcon>
<Typography variant="inherit">X-Axis range (Time)</Typography>
</MenuItem>
<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} data-menu-item="Size">
<ListItemIcon>
<Sort />
</ListItemIcon>
<Typography variant="inherit">Size</Typography>
</MenuItem>
<MenuItem key="color" onClick={toggleColor} data-menu-item="Color">
<ListItemIcon>
<ColorLens />
</ListItemIcon>
<Typography variant="inherit">Color</Typography>
</MenuItem>
<MenuItem key="clear" onClick={props.resetDataAction} data-menu-item="Clear data">
<ListItemIcon>
<Clear />
</ListItemIcon>
<Typography variant="inherit">Clear data</Typography>
</MenuItem>
<MoveUp chart={props.chart} close={props.close} />
</Menu>
<RangeSettings chart={props.chart} anchorEl={props.anchorEl.current} open={rangeVisible} onClose={toggleRange} />
<TimeRangeSettings
chart={props.chart}
anchorEl={props.anchorEl.current}
open={timeRangeVisible}
onClose={toggleTimeRange}
/>
<InterpolationSettings
chart={props.chart}
anchorEl={props.anchorEl.current}
open={interpolationVisible}
close={toggleInterpolation}
/>
<Size chart={props.chart} anchorEl={props.anchorEl.current} open={sizeVisible} close={toggleSize} />
<ColorSettings chart={props.chart} anchorEl={props.anchorEl.current} open={colorVisible} close={toggleColor} />
</span>
)
}
export default memo(ChartSettings)
@@ -0,0 +1,31 @@
import * as React from 'react'
import { ChartParameters } from '../../reducers/Charts'
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
return (
<div style={{ flexGrow: 1, overflow: 'hidden', textOverflow: 'ellipsis' }}>
<Typography variant="caption" className={classes.topic}>
{parameters.dotPath ? parameters.dotPath : parameters.topic}
</Typography>
<br />
<Typography variant="caption" className={classes.topic}>
{parameters.dotPath ? parameters.topic : <span dangerouslySetInnerHTML={{ __html: '&nbsp;' }}></span>}
</Typography>
</div>
)
}
const styles = (theme: Theme) => ({
topic: {
wordBreak: 'break-all' as 'break-all',
whiteSpace: 'nowrap' as 'nowrap',
overflow: 'hidden' as 'hidden',
textOverflow: 'ellipsis' as 'ellipsis',
},
})
export default withStyles(styles)(ChartTitle)
@@ -0,0 +1,20 @@
import * as q from '../../../../backend/src/Model'
import React from 'react'
import TopicChart from './TopicChart'
import { ChartParameters } from '../../reducers/Charts'
import { usePollingToFetchTreeNode } from '../helper/usePollingToFetchTreeNode'
interface Props {
tree?: q.Tree<any>
parameters: ChartParameters
}
export function ChartWithTreeNode(props: Props) {
const { tree, parameters } = props
if (!tree) {
return null
}
const treeNode = usePollingToFetchTreeNode(tree, parameters.topic)
return <TopicChart treeNode={treeNode} parameters={parameters} />
}
@@ -0,0 +1,137 @@
import * as q from '../../../../backend/src/Model'
import ChartTitle from './ChartTitle'
import React, { useState, useCallback, memo, useRef } from 'react'
import TopicPlot from '../TopicPlot'
import { bindActionCreators } from 'redux'
import { ChartActions } from './ChartActions'
import { chartActions } from '../../actions'
import { ChartParameters } from '../../reducers/Charts'
import { connect } from 'react-redux'
import { Paper } from '@mui/material'
const throttle = require('lodash.throttle')
class ClearableMessageBuffer extends q.RingBuffer<q.Message> {
public clear() {
this.items = []
this.start = 0
this.end = 0
}
public static fromMessageBuffer(buffer: q.RingBuffer<q.Message>): ClearableMessageBuffer {
return new ClearableMessageBuffer(buffer.capacity, buffer.maxItems, buffer.compactionFactor, buffer)
}
public clone(): ClearableMessageBuffer {
return ClearableMessageBuffer.fromMessageBuffer(this)
}
}
interface Props {
parameters: ChartParameters
treeNode?: q.TreeNode<any>
actions: {
chart: typeof chartActions
}
}
/**
* Subscribes to onMessages, stores more data points then the default
*/
function useMessageSubscriptionToUpdate(treeNode?: q.TreeNode<any>) {
const [lastUpdated, setLastUpdate] = useState(0)
const [messageHistory, setMessageHistory] = useState<ClearableMessageBuffer | undefined>()
let amendMessageCallback: any
function subscribeToMessageUpdates() {
const throttledUpdate = throttle(() => setLastUpdate(treeNode ? treeNode.lastUpdate : 0), 300)
if (treeNode) {
const newMessageHistory = ClearableMessageBuffer.fromMessageBuffer(treeNode.messageHistory)
newMessageHistory.setCapacity(500, 2 * 500 * 10000)
amendMessageCallback = (message: q.Message) => {
newMessageHistory.add(message)
throttledUpdate()
}
treeNode.onMessage.subscribe(amendMessageCallback)
setMessageHistory(newMessageHistory)
}
return function cleanup() {
treeNode && treeNode.onMessage.unsubscribe(amendMessageCallback)
setMessageHistory(undefined)
}
}
React.useEffect(subscribeToMessageUpdates, [treeNode])
return messageHistory
}
function useResetDataCallback(messageHistory: ClearableMessageBuffer | undefined) {
const [lastUpdated, setLastUpdate] = useState(0)
return React.useCallback(() => {
messageHistory && messageHistory.clear()
setLastUpdate(Date.now())
}, [messageHistory])
}
function TopicChart(props: Props) {
const { parameters, treeNode } = props
const [frozenHistory, setFrozenHistory] = useState<q.MessageHistory | undefined>()
const messageHistory = useMessageSubscriptionToUpdate(treeNode)
const togglePause = useCallback(() => {
if (!treeNode) {
return
}
setFrozenHistory(frozenHistory ? undefined : messageHistory && messageHistory.clone())
}, [props.treeNode, frozenHistory, messageHistory])
const onRemove = React.useCallback(() => {
props.actions.chart.removeChart(props.parameters)
}, [props.parameters])
const resetData = useResetDataCallback(messageHistory)
return (
<Paper
style={{ padding: '8px' }}
data-test-type="ChartPaper"
data-test={`${props.parameters.topic}-${props.parameters.dotPath || ''}`}
>
<div style={{ display: 'flex' }}>
<div style={{ display: 'flex', flexGrow: 1, overflow: 'hidden' }}>
<ChartTitle parameters={parameters} />
<ChartActions
resetDataAction={resetData}
parameters={parameters}
onRemove={onRemove}
paused={Boolean(frozenHistory)}
togglePause={togglePause}
/>
</div>
</div>
<TopicPlot
node={props.treeNode ? props.treeNode : undefined}
color={props.parameters.color}
interpolation={props.parameters.interpolation}
timeInterval={props.parameters.timeRange ? props.parameters.timeRange.until : undefined}
range={props.parameters.range ? [props.parameters.range.from, props.parameters.range.to] : undefined}
history={frozenHistory || messageHistory || new ClearableMessageBuffer(1)}
dotPath={parameters.dotPath}
/>
</Paper>
)
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
chart: bindActionCreators(chartActions, dispatch),
},
}
}
export default connect(undefined, mapDispatchToProps)(memo(TopicChart))
+131
View File
@@ -0,0 +1,131 @@
import * as q from '../../../../backend/src/Model'
import * as React from 'react'
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, 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')
interface Props {
charts: List<ChartParameters>
connectionId?: string
tree?: q.Tree<any>
actions: {
chart: typeof chartActions
}
classes: any
}
function spacingForChartCount(count: number): 4 | 6 | 12 {
if (count >= 5) {
return 4
} else if (count >= 2) {
return 6
} else {
return 12
}
}
function mapWidth(width: 'big' | 'medium' | 'small' | undefined, calculatedSpacing: 4 | 6 | 12): 4 | 6 | 12 {
switch (width) {
case 'big':
return 12
case 'medium':
return 6
case 'small':
return 4
default:
return calculatedSpacing
}
}
function ChartPanel(props: Props) {
const chartsInView = props.charts.count()
const [spacing, setSpacing] = React.useState(spacingForChartCount(chartsInView))
React.useEffect(() => {
props.actions.chart.loadCharts()
}, [props.connectionId])
// Update spacing after animations have completed
React.useEffect(() => {
const newSpacing = spacingForChartCount(chartsInView)
if (newSpacing > spacing) {
setTimeout(() => setSpacing(newSpacing), 500)
} else {
setSpacing(newSpacing)
}
}, [chartsInView])
const charts = props.charts.map(chartParameters => (
<CSSTransition
key={`${chartParameters.topic}-${chartParameters.dotPath || ''}`}
timeout={{ enter: 500, exit: 500 }}
classNames="example"
>
<Grid item xs={mapWidth(chartParameters.width, spacing)}>
<ChartWithTreeNode tree={props.tree} parameters={chartParameters} />
</Grid>
</CSSTransition>
))
return (
<div className={props.classes.container}>
<Grid container spacing={1}>
<TransitionGroup component={null} className="example">
{charts}
</TransitionGroup>
{chartsInView === 0 ? <NoCharts key="noCharts" /> : null}
</Grid>
</div>
)
}
function NoCharts() {
return (
<div style={{ width: '100%', textAlign: 'center' }}>
<Typography variant="h2">No charts selected</Typography>
<Typography>Select a numeric values from the value preview.</Typography>
<Typography>
Click on <ShowChart /> to add a topic / value to this panel.
</Typography>
</div>
)
}
const mapStateToProps = (state: AppState) => {
return {
charts: state.charts.get('charts'),
connectionId: state.connection.connectionId,
tree: state.connection.tree,
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
chart: bindActionCreators(chartActions, dispatch),
},
}
}
const styles = (theme: Theme) => ({
container: {
backgroundColor: theme.palette.background.default,
width: '100%',
height: '100%',
padding: '8px',
flex: 1,
overflow: 'hidden scroll',
},
})
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ChartPanel) as any)
+61
View File
@@ -0,0 +1,61 @@
import React, { useRef, useCallback, memo } from 'react'
import { ConfirmationRequest } from '../reducers/Global'
import { Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, Button } from '@mui/material'
import { KeyCodes } from '../utils/KeyCodes'
function ConfirmationDialog(props: { confirmationRequests: Array<ConfirmationRequest> }) {
const request = props.confirmationRequests[0]
const yesRef = useRef<HTMLButtonElement>()
const noRef = useRef<HTMLButtonElement>()
const arrowKeyHandler = useCallback((event: React.KeyboardEvent) => {
const isArrowKey = event.keyCode === KeyCodes.arrow_left || event.keyCode === KeyCodes.arrow_right
if (!isArrowKey) {
return
}
event.stopPropagation()
if (document.activeElement === noRef.current) {
yesRef.current && yesRef.current.focus()
} else {
noRef.current && noRef.current.focus()
}
}, [])
const confirm = React.useCallback(() => {
request && request.callback(true)
}, [request])
const reject = React.useCallback(() => {
request && request.callback(false)
}, [request])
if (!request) {
return null
}
return (
<Dialog
open={true}
onClose={reject}
aria-labelledby="alert-dialog-title"
aria-describedby="alert-dialog-description"
onKeyDown={arrowKeyHandler}
>
<DialogTitle id="alert-dialog-title">{request.title}</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description" style={{ whiteSpace: 'pre-wrap' }}>
{request.inquiry}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button ref={yesRef as any} variant="contained" onClick={confirm} color="primary" autoFocus>
Yes
</Button>
<Button ref={noRef as any} variant="contained" onClick={reject} color="secondary" style={{ marginLeft: '8px' }}>
No
</Button>
</DialogActions>
</Dialog>
)
}
export default memo(ConfirmationDialog)
@@ -0,0 +1,136 @@
import * as React from 'react'
import { useState, useCallback, memo } from 'react'
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 } 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
classes: any
managerActions: typeof connectionManagerActions
}
const ConnectionSettings = memo(function ConnectionSettings(props: Props) {
const [qos, setQos] = useState<QoS>(0)
const [topic, setTopic] = useState('')
const { classes } = props
const updateSubscription = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => setTopic(event.target.value),
[]
)
const handleChange = useCallback(
(name: string) => (event: any) => {
props.managerActions.updateConnection(props.connection.id, {
[name]: event.target.value,
})
},
[]
)
return (
<div>
<form className={classes.container} noValidate={true} autoComplete="off">
<Grid container={true} spacing={3}>
<Grid item={true} xs={8} className={classes.gridPadding}>
<TextField
className={`${classes.fullWidth} advanced-connection-settings-topic-input`}
label="Topic"
placeholder="example/topic"
margin="normal"
value={topic}
onChange={updateSubscription}
/>
</Grid>
<Grid item={true} xs={2} className={classes.gridPadding}>
<div className={classes.qos}>
<QosSelect label="QoS" selected={qos} onChange={setQos} />
</div>
</Grid>
<Grid item={true} xs={2} className={classes.gridPadding}>
<Button
className={classes.button}
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 }}>
<SubscriptionsAny connection={props.connection} />
</Grid>
<Grid item={true} xs={7} className={classes.gridPadding}>
<TextField
className={classes.fullWidth}
label="MQTT Client ID"
margin="normal"
value={props.connection.clientId}
onChange={handleChange('clientId')}
/>
</Grid>
<Grid item={true} xs={3} className={classes.gridPadding}>
<div>
<Tooltip title="Manage tls connection certificates" placement="top">
<Button
variant="contained"
className={classes.button}
onClick={() => props.managerActions.toggleCertificateSettings()}
>
<Lock /> Certificates
</Button>
</Tooltip>
</div>
</Grid>
<Grid item={true} xs={2} className={classes.gridPadding}>
<Button
variant="contained"
className={classes.button}
onClick={props.managerActions.toggleAdvancedSettings}
data-testid="back-button"
>
<Undo /> Back
</Button>
</Grid>
</Grid>
</form>
</div>
)
})
const mapDispatchToProps = (dispatch: any) => {
return {
managerActions: bindActionCreators(connectionManagerActions, dispatch),
}
}
const styles = (theme: Theme) => ({
fullWidth: {
width: '100%',
},
gridPadding: {
padding: '0 12px !important',
},
button: {
marginTop: theme.spacing(3),
float: 'right' as 'right',
},
qos: {
marginTop: theme.spacing(1),
},
})
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(ConnectionSettings) as any)
@@ -0,0 +1,140 @@
import * as React from 'react'
import ClearAdornment from '../helper/ClearAdornment'
import Lock from '@mui/icons-material/Lock'
import { bindActionCreators } from 'redux'
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 '@mui/styles'
import { rendererRpc } from '../../../../events'
import { RpcEvents } from '../../../../events/EventsV2'
function BrowserCertificateFileSelection(props: {
certificateType: CertificateTypes
title: string
certificate?: CertificateParameters
classes: any
actions: {
connectionManager: typeof connectionManagerActions
}
connection: ConnectionOptions
}) {
const fileInputRef = React.useRef<HTMLInputElement>(null)
const clearCertificate = React.useCallback(() => {
props.actions.connectionManager.updateConnection(props.connection.id, {
[props.certificateType]: undefined,
})
}, [props.connection, props.certificateType])
const handleFileSelect = React.useCallback(
async (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0]
if (!file) {
return
}
try {
// Read file content
const reader = new FileReader()
reader.onload = async e => {
const content = e.target?.result
if (typeof content === 'string') {
// Convert to base64
const base64Data = content.split(',')[1] || content
// Upload via IPC instead of HTTP POST
const result = await rendererRpc.call(RpcEvents.uploadCertificate, {
filename: file.name,
data: base64Data,
})
// Create certificate parameters
const certificate: CertificateParameters = {
name: result.name,
data: result.data,
}
// Update connection
props.actions.connectionManager.updateConnection(props.connection.id, {
[props.certificateType]: certificate,
})
}
}
reader.readAsDataURL(file)
} catch (error) {
console.error('Error uploading certificate:', error)
}
// Reset input
if (fileInputRef.current) {
fileInputRef.current.value = ''
}
},
[props.connection.id, props.certificateType, props.actions.connectionManager]
)
const handleButtonClick = () => {
fileInputRef.current?.click()
}
return (
<span>
<input
ref={fileInputRef}
type="file"
accept=".pem,.crt,.cer,.key"
style={{ display: 'none' }}
onChange={handleFileSelect}
/>
<Tooltip title="Select certificate" placement="top">
<Button variant="contained" className={props.classes.button} onClick={handleButtonClick}>
<Lock /> {props.title}
</Button>
</Tooltip>
<ClearCertificate classes={props.classes} certificate={props.certificate} action={clearCertificate} />
</span>
)
}
function ClearCertificate(props: { classes: any; certificate?: CertificateParameters; action: () => void }) {
if (!props.certificate) {
return null
}
return (
<Tooltip title={props.certificate.name}>
<Typography className={props.classes.certificateName}>
<ClearAdornment action={props.action} value={props.certificate.name} />
{props.certificate.name}
</Typography>
</Tooltip>
)
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
connectionManager: bindActionCreators(connectionManagerActions, dispatch),
},
}
}
const styles = (theme: Theme) => ({
certificateName: {
width: '100%',
height: 'calc(1em + 4px)',
overflow: 'hidden' as 'hidden',
whiteSpace: 'nowrap' as 'nowrap',
textOverflow: 'ellipsis' as 'ellipsis',
color: theme.palette.text.secondary,
},
button: {
marginTop: theme.spacing(3),
marginRight: theme.spacing(2),
},
})
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(BrowserCertificateFileSelection) as any)
@@ -0,0 +1,82 @@
import * as React from 'react'
import ClearAdornment from '../helper/ClearAdornment'
import Lock from '@mui/icons-material/Lock'
import { bindActionCreators } from 'redux'
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 '@mui/styles'
function CertificateFileSelection(props: {
certificateType: CertificateTypes
title: string
certificate?: CertificateParameters
classes: any
actions: {
connectionManager: typeof connectionManagerActions
}
connection: ConnectionOptions
}) {
const clearCertificate = React.useCallback(() => {
props.actions.connectionManager.updateConnection(props.connection.id, {
[props.certificateType]: undefined,
})
}, [props.connection, props.certificateType])
return (
<span>
<Tooltip title="Select certificate" placement="top">
<Button
variant="contained"
className={props.classes.button}
onClick={() => props.actions.connectionManager.selectCertificate(props.certificateType, props.connection.id)}
>
<Lock /> {props.title}
</Button>
</Tooltip>
<ClearCertificate classes={props.classes} certificate={props.certificate} action={clearCertificate} />
</span>
)
}
function ClearCertificate(props: { classes: any; certificate?: CertificateParameters; action: () => void }) {
if (!props.certificate) {
return null
}
return (
<Tooltip title={props.certificate.name}>
<Typography className={props.classes.certificateName}>
<ClearAdornment action={props.action} value={props.certificate.name} />
{props.certificate.name}
</Typography>
</Tooltip>
)
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
connectionManager: bindActionCreators(connectionManagerActions, dispatch),
},
}
}
const styles = (theme: Theme) => ({
certificateName: {
width: '100%',
height: 'calc(1em + 4px)',
overflow: 'hidden' as 'hidden',
whiteSpace: 'nowrap' as 'nowrap',
textOverflow: 'ellipsis' as 'ellipsis',
color: theme.palette.text.secondary,
},
button: {
marginTop: theme.spacing(3),
marginRight: theme.spacing(2),
},
})
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(CertificateFileSelection) as any)
@@ -0,0 +1,113 @@
import * as React from 'react'
import CertificateFileSelection from './CertificateFileSelection'
import BrowserCertificateFileSelection from './BrowserCertificateFileSelection'
import Undo from '@mui/icons-material/Undo'
import { bindActionCreators } from 'redux'
import { Button, Grid } from '@mui/material'
import { connect } from 'react-redux'
import { connectionManagerActions } from '../../actions'
import { ConnectionOptions } from '../../model/ConnectionOptions'
import { Theme } from '@mui/material/styles'
import { withStyles } from '@mui/styles'
import { isBrowserMode } from '../../utils/browserMode'
// Use browser or desktop file selection based on mode
const CertSelector: any = isBrowserMode ? BrowserCertificateFileSelection : CertificateFileSelection
interface Props {
connection: ConnectionOptions
classes: any
managerActions: typeof connectionManagerActions
}
interface State {
subscription: string
}
class Certificates extends React.PureComponent<Props, State> {
constructor(props: any) {
super(props)
this.state = { subscription: '' }
}
private handleChange = (name: string) => (event: any) => {
this.props.managerActions.updateConnection(this.props.connection.id, {
[name]: event.target.value,
})
}
private renderCertificateInfo() {
if (!this.props.connection.selfSignedCertificate) {
return null
}
return <span />
}
public render() {
const { classes } = this.props
return (
<div>
<form noValidate={true} autoComplete="off">
<Grid container={true} spacing={3}>
<Grid item={true} xs={12} className={classes.gridPadding}>
<CertSelector
connection={this.props.connection}
certificate={this.props.connection.selfSignedCertificate}
title="Server Certificate (CA)"
certificateType="selfSignedCertificate"
/>
</Grid>
<Grid item={true} xs={12} className={classes.gridPadding}>
<CertSelector
connection={this.props.connection}
certificate={this.props.connection.clientCertificate}
title="Client Certificate"
certificateType="clientCertificate"
/>
</Grid>
<Grid item={true} xs={12} className={classes.gridPadding}>
<CertSelector
connection={this.props.connection}
certificate={this.props.connection.clientKey}
title="Client Key"
certificateType="clientKey"
/>
</Grid>
<Grid item={true} xs={2} className={classes.gridPadding}>
<br />
<Button
variant="contained"
className={classes.button}
onClick={this.props.managerActions.toggleCertificateSettings}
>
<Undo /> Back
</Button>
</Grid>
</Grid>
</form>
</div>
)
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
managerActions: bindActionCreators(connectionManagerActions, dispatch),
}
}
const styles = (theme: Theme) => ({
fullWidth: {
width: '100%',
},
gridPadding: {
padding: '0 12px !important',
},
button: {
marginTop: theme.spacing(3),
marginRight: theme.spacing(2),
},
})
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(Certificates) as any)
@@ -0,0 +1,25 @@
import ConnectionHealthIndicator from '../helper/ConnectionHealthIndicator'
import PowerSettingsNew from '@mui/icons-material/PowerSettingsNew'
import React from 'react'
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} data-testid="abort-button">
<ConnectionHealthIndicator />
&nbsp;&nbsp;Abort
</Button>
)
}
return (
<Button variant="contained" color="primary" className={classes.button} onClick={toggle} data-testid="connect-button">
<PowerSettingsNew /> Connect
</Button>
)
}
export default ConnectButton
@@ -1,395 +0,0 @@
import * as React from 'react'
import {
Button,
CircularProgress,
FormControl,
FormControlLabel,
Grid,
IconButton,
Input,
InputAdornment,
InputLabel,
MenuItem,
Modal,
Paper,
Switch,
TextField,
Toolbar,
Typography,
} from '@material-ui/core'
import { connect } from 'react-redux'
import { MqttOptions } from '../../../../backend/src/DataSource'
import { StyleRulesCallback, Theme, withStyles } from '@material-ui/core/styles'
import Notification from './Notification'
import Visibility from '@material-ui/icons/Visibility'
import VisibilityOff from '@material-ui/icons/VisibilityOff'
const sha1 = require('sha1')
import { AppState } from '../../reducers'
import { bindActionCreators } from 'redux'
import { connectionActions } from '../../actions'
interface Props {
classes: {[s: string]: string}
actions: typeof connectionActions,
visible: boolean
connected: boolean
connecting: boolean
error?: string
}
const protocols = [
'mqtt://',
'ws://',
]
interface State {
showPassword: boolean
connectionSettings: ConnectionSettings
}
interface ConnectionSettings {
host: string
protocol: string
port: number
tls: boolean
certValidation: boolean
clientId: string
connectionId?: string
username: string
password: string
}
declare var window: any
class Connection extends React.Component<Props, State> {
private randomClientId: string
private defaultConnectionSettings: ConnectionSettings = {
host: 'iot.eclipse.org',
protocol: protocols[0],
port: 1883,
tls: false,
certValidation: true,
clientId: '',
username: '',
password: '',
connectionId: undefined,
}
constructor(props: any) {
super(props)
const clientIdSha = sha1(`${Math.random()}`).slice(0, 8)
this.randomClientId = `mqtt-explorer-${clientIdSha}`
this.state = {
connectionSettings: this.loadConnectionSettings(),
showPassword: false,
}
}
private loadConnectionSettings(): ConnectionSettings {
let storedSettings: ConnectionSettings | undefined
const storedSettingsString = window.localStorage.getItem('connectionSettings')
try {
storedSettings = storedSettingsString ? JSON.parse(storedSettingsString) : undefined
} catch {
window.localStorage.setItem('connectionSettings', undefined)
}
return storedSettings || this.defaultConnectionSettings
}
private saveConnectionSettings() {
window.localStorage.setItem('connectionSettings', JSON.stringify(this.state.connectionSettings))
}
private handleClickShowPassword = () => {
this.setState({ showPassword: !this.state.showPassword })
}
private optionsFromState(): MqttOptions {
const protocol = this.state.connectionSettings.protocol === 'tcp://' ? 'mqtt://' : this.state.connectionSettings.protocol
const url = `${protocol}${this.state.connectionSettings.host}:${this.state.connectionSettings.port}`
return {
url,
username: this.state.connectionSettings.username || undefined,
password: this.state.connectionSettings.password || undefined,
clientId: this.state.connectionSettings.clientId || this.randomClientId,
tls: this.state.connectionSettings.tls,
certValidation: this.state.connectionSettings.certValidation,
}
}
public static styles: StyleRulesCallback<string> = (theme: Theme) => {
return {
root: {
minWidth: 550,
maxWidth: 650,
backgroundColor: theme.palette.background.default,
margin: '14vh auto auto auto',
padding: `${2 * theme.spacing.unit}px`,
outline: 'none',
},
title: {
color: theme.palette.text.primary,
},
paper: {
padding: theme.spacing.unit * 2,
textAlign: 'center',
color: theme.palette.text.secondary,
},
textField: {
width: '100%',
},
switch: {
marginTop: `${1 * theme.spacing.unit}px`,
},
button: {
margin: theme.spacing.unit,
},
inputFormControl: {
marginTop: '16px',
},
}
}
private handleChange = (name: string) => (event: any) => {
this.setState({
connectionSettings: {
...this.state.connectionSettings,
[name]: event.target.value,
},
})
}
public render() {
const { classes } = this.props
const passwordVisibilityButton = (
<InputAdornment position="end">
<IconButton
aria-label="Toggle password visibility"
onClick={this.handleClickShowPassword}
>
{this.state.showPassword ? <Visibility /> : <VisibilityOff />}
</IconButton>
</InputAdornment>
)
let renderError = null
if (this.props.error) {
renderError = (
<Notification
message={this.props.error}
onClose={() => { this.props.actions.showError(undefined) }}
/>
)
}
return (
<div>
{renderError}
<Modal open={this.props.visible} disableAutoFocus={true}>
<Paper className={classes.root}>
<Toolbar>
<Typography className={classes.title} variant="h6" color="inherit">MQTT Connection</Typography>
</Toolbar>
<form className={classes.container} noValidate={true} autoComplete="off">
<Grid container={true} spacing={24}>
<Grid item={true} xs={2}>
{this.renderProtocols()}
</Grid>
<Grid item={true} xs={7}>
<TextField
label="Host"
className={classes.textField}
value={this.state.connectionSettings.host}
onChange={this.handleChange('host')}
margin="normal"
/>
</Grid>
<Grid item={true} xs={3}>
<TextField
label="Port"
className={classes.textField}
value={this.state.connectionSettings.port}
onChange={this.handleChange('port')}
margin="normal"
/>
</Grid>
<Grid item={true} xs={5}>
<TextField
label="Username"
className={classes.textField}
value={this.state.connectionSettings.username}
onChange={this.handleChange('username')}
margin="normal"
/>
</Grid>
<Grid item={true} xs={5}>
<FormControl className={`${classes.textField} ${classes.inputFormControl}`}>
<InputLabel htmlFor="adornment-password">Password</InputLabel>
<Input
id="adornment-password"
type={this.state.showPassword ? 'text' : 'password'}
value={this.state.connectionSettings.password}
onChange={this.handleChange('password')}
endAdornment={passwordVisibilityButton}
/>
</FormControl>
</Grid>
<Grid item={true} xs={5}>
<FormControl className={`${classes.textField} ${classes.inputFormControl}`}>
<InputLabel htmlFor="client-id">Client ID</InputLabel>
<Input
placeholder={this.randomClientId}
className={classes.textField}
value={this.state.connectionSettings.clientId || ''}
onChange={this.handleChange('clientId')}
startAdornment={<span />}
/>
</FormControl>
</Grid>
<Grid item={true} xs={4}>
{this.renderCertValidationSwitch()}
</Grid>
<Grid item={true} xs={3}>
{this.renderTlsSwitch()}
</Grid>
</Grid>
<br />
<div style={{ textAlign: 'right' }}>
<Button variant="contained" color="secondary" className={classes.button} onClick={() => this.saveConnectionSettings()}>
Save
</Button>
{this.renderConnectButton()}
</div>
</form>
</Paper>
</Modal>
</div>
)
}
private renderProtocols() {
const { classes } = this.props
const protocolItems = protocols.map((value: string) => (
<MenuItem key={value} value={value}>
{value}
</MenuItem>
))
return (
<TextField
select={true}
label="Protocol"
className={classes.textField}
value={this.state.connectionSettings.protocol}
onChange={this.handleChange('protocol')}
margin="normal"
>
{protocolItems}
</TextField>
)
}
private renderCertValidationSwitch() {
const { classes } = this.props
const certSwitch = (
<Switch
checked={this.state.connectionSettings.certValidation}
onChange={this.toggleCertValidation}
color="primary"
/>
)
return (
<div className={classes.switch}>
<FormControlLabel
control={certSwitch}
label="Validate certificate"
labelPlacement="bottom"
/>
</div>
)
}
private toggleCertValidation = () => this.setState({
connectionSettings: {
...this.state.connectionSettings,
certValidation: !this.state.connectionSettings.certValidation,
},
})
private renderTlsSwitch() {
const { classes } = this.props
const tlsSwitch = (
<Switch
checked={this.state.connectionSettings.tls}
onChange={this.toggleTls}
color="primary"
/>
)
return (
<div className={classes.switch}>
<FormControlLabel
control={tlsSwitch}
label="Encryption (tls)"
labelPlacement="bottom"
/>
</div>
)
}
private toggleTls = () => this.setState({
connectionSettings: {
...this.state.connectionSettings,
tls: !this.state.connectionSettings.tls,
},
})
private renderConnectButton() {
const { classes, actions } = this.props
if (this.props.connecting) {
return (
<Button variant="contained" color="primary" className={classes.button} onClick={actions.disconnect}>
<CircularProgress size={22} style={{ marginRight: '10px' }} color="secondary" /> Abort
</Button>
)
}
return (
<Button variant="contained" color="primary" className={classes.button} onClick={this.onClickConnect}>
Connect
</Button>
)
}
private onClickConnect = () => {
const connectionId = String(sha1(String(Math.random())).slice(0, 8))
const options = this.optionsFromState()
this.props.actions.connect(options, connectionId)
}
}
const mapStateToProps = (state: AppState) => {
return {
visible: !state.connection.connected,
connected: state.connection.connected,
connecting: state.connection.connecting,
error: state.connection.error,
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(connectionActions, dispatch),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(Connection.styles)(Connection))
@@ -0,0 +1,290 @@
import ConnectButton from './ConnectButton'
import React, { useCallback, useState } from 'react'
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 } from '@mui/material/styles'
import { withStyles } from '@mui/styles'
import { ToggleSwitch } from './ToggleSwitch'
import { useGlobalKeyEventHandler } from '../../effects/useGlobalKeyEventHandler'
import {
Button,
FormControl,
Grid,
IconButton,
Input,
InputAdornment,
InputLabel,
MenuItem,
TextField,
} from '@mui/material'
interface Props {
connection: ConnectionOptions
classes: { [s: string]: string }
actions: typeof connectionActions
managerActions: typeof connectionManagerActions
connected: boolean
connecting: boolean
}
const protocols = ['mqtt', 'ws']
function ConnectionSettings(props: Props) {
const [showPassword, setShowPassword] = useState(false)
const toggleConnect = useCallback(() => {
if (props.connecting) {
props.actions.disconnect()
return
}
if (!props.connection) {
return
}
const mqttOptions = toMqttConnection(props.connection)
if (mqttOptions) {
props.actions.connect(mqttOptions, props.connection.id)
}
}, [props.connection, props.connecting])
useGlobalKeyEventHandler(KeyCodes.escape, props.actions.disconnect)
useGlobalKeyEventHandler(KeyCodes.enter, toggleConnect, [props.connecting])
const handleClickShowPassword = useCallback(() => {
setShowPassword(!showPassword)
}, [showPassword])
function requiresBasePath() {
return props.connection.protocol !== 'mqtt'
}
function renderBasePathInput() {
return (
<Grid item={true} xs={4}>
<TextField
label="Basepath"
className={props.classes.textField}
value={props.connection.basePath}
onChange={handleChange('basePath')}
margin="normal"
/>
</Grid>
)
}
const handleChange = (name: string) => (event: any) => {
if (!props.connection) {
return
}
updateConnection(name, event.target.value)
}
const updateConnection = (name: string, value: any) => {
props.managerActions.updateConnection(props.connection.id, {
[name]: value,
})
}
const renderProtocols = () => {
const { classes, connection } = props
const protocolItems = protocols.map((value: string) => (
<MenuItem key={value} value={value}>
{value}://
</MenuItem>
))
return (
<TextField
select={true}
label="Protocol"
className={classes.textField}
value={connection.protocol}
onChange={updateProtocol}
margin="normal"
>
{protocolItems}
</TextField>
)
}
const updateProtocol = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value
updateConnection('protocol', value)
if (event.target.value === 'mqtt') {
updateConnection('basePath', undefined)
} else {
updateConnection('basePath', 'ws')
}
}
const toggleCertValidation = () => {
props.managerActions.updateConnection(props.connection.id, {
certValidation: !props.connection.certValidation,
})
}
const toggleTls = () => {
props.managerActions.updateConnection(props.connection.id, {
encryption: !props.connection.encryption,
})
}
function PasswordVisibilityButton(props: { showPassword: boolean; toggle: () => void }) {
return (
<InputAdornment position="end">
<IconButton aria-label="Toggle password visibility" onClick={props.toggle}>
{props.showPassword ? <Visibility /> : <VisibilityOff />}
</IconButton>
</InputAdornment>
)
}
const { classes, connection } = props
return (
<div>
<form className={classes.container} noValidate={true} autoComplete="off">
<Grid container={true} spacing={3}>
<Grid item={true} xs={5}>
<TextField
autoFocus={true}
label="Name"
className={classes.textField}
value={connection.name}
onChange={handleChange('name')}
margin="normal"
/>
</Grid>
<Grid item={true} xs={4}>
<ToggleSwitch
label="Validate certificate"
classes={classes}
value={connection.certValidation}
toggle={toggleCertValidation}
/>
</Grid>
<Grid item={true} xs={3}>
<ToggleSwitch label="Encryption (tls)" classes={classes} value={connection.encryption} toggle={toggleTls} />
</Grid>
<Grid item={true} xs={2}>
{renderProtocols()}
</Grid>
<Grid item={true} xs={7}>
<TextField
label="Host"
className={classes.textField}
value={connection.host}
onChange={handleChange('host')}
margin="normal"
/>
</Grid>
<Grid item={true} xs={3}>
<TextField
label="Port"
className={classes.textField}
value={connection.port}
onChange={handleChange('port')}
margin="normal"
/>
</Grid>
{requiresBasePath() ? renderBasePathInput() : null}
<Grid item={true} xs={requiresBasePath() ? 4 : 6}>
<TextField
label="Username"
className={classes.textField}
value={connection.username}
onChange={handleChange('username')}
margin="normal"
/>
</Grid>
<Grid item={true} xs={requiresBasePath() ? 4 : 6}>
<FormControl className={`${classes.textField} ${classes.inputFormControl}`}>
<InputLabel htmlFor="adornment-password">Password</InputLabel>
<Input
id="adornment-password"
type={showPassword ? 'text' : 'password'}
value={connection.password}
onChange={handleChange('password')}
endAdornment={<PasswordVisibilityButton showPassword={showPassword} toggle={handleClickShowPassword} />}
/>
</FormControl>
</Grid>
</Grid>
<br />
<div>
<div style={{ float: 'left' }}>
<Button
variant="contained"
className={classes.button}
onClick={() => props.managerActions.deleteConnection(props.connection.id)}
>
Delete <Delete />
</Button>
<Button
variant="contained"
className={classes.button}
onClick={props.managerActions.toggleAdvancedSettings}
data-testid="advanced-button"
>
<Settings /> Advanced
</Button>
</div>
<div style={{ float: 'right' }}>
<Button
variant="contained"
color="secondary"
className={classes.button}
onClick={props.managerActions.saveConnectionSettings}
>
<Save /> Save
</Button>
<ConnectButton toggle={toggleConnect} connecting={props.connecting} classes={classes} />
</div>
</div>
</form>
</div>
)
}
const mapStateToProps = (state: AppState) => {
return {
connected: state.connection.connected,
connecting: state.connection.connecting,
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(connectionActions, dispatch),
managerActions: bindActionCreators(connectionManagerActions, dispatch),
}
}
const styles = (theme: Theme) => ({
textField: {
width: '100%',
},
switch: {
marginTop: 0,
},
button: {
margin: theme.spacing(1),
},
inputFormControl: {
marginTop: '16px',
},
})
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ConnectionSettings) as any)
@@ -0,0 +1,141 @@
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 } 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
classes: any
connection?: ConnectionOptions
visible: boolean
showAdvancedSettings: boolean
showCertificateSettings: boolean
}
class ConnectionSetup extends React.PureComponent<Props, {}> {
constructor(props: Props) {
super(props)
}
private renderSettings() {
const { connection, showAdvancedSettings, showCertificateSettings } = this.props
if (!connection) {
return null
}
return (
<div>
<Collapse in={!showAdvancedSettings && !showCertificateSettings}>
<ConnectionSettingsAny connection={connection} />
</Collapse>
<Collapse in={showAdvancedSettings && !showCertificateSettings}>
<AdvancedConnectionSettingsAny connection={connection} />
</Collapse>
<Collapse in={showCertificateSettings}>
<CertificatesAny connection={connection} />
</Collapse>
</div>
)
}
public componentDidMount() {
this.props.actions.loadConnectionSettings()
}
public render() {
const { classes, visible, connection } = this.props
const mqttConnection = connection && toMqttConnection(connection)
return (
<div>
<Modal open={visible} disableAutoFocus={true}>
<Paper className={classes.root}>
<div className={classes.left}>
<ProfileList />
</div>
<div className={classes.right} key={connection && connection.id}>
<Toolbar>
<Typography className={classes.title} variant="h6" color="inherit">
MQTT Connection
</Typography>
<Typography className={classes.connectionUri}>{mqttConnection && mqttConnection.url}</Typography>
</Toolbar>
{this.renderSettings()}
</div>
</Paper>
</Modal>
</div>
)
}
}
const connectionHeight = '440px'
const styles = (theme: Theme) => ({
title: {
color: theme.palette.text.primary,
whiteSpace: 'nowrap' as 'nowrap',
},
root: {
margin: `calc((100vh - ${connectionHeight}) / 2) auto 0 auto`,
minWidth: '800px',
maxWidth: '850px',
height: connectionHeight,
outline: 'none' as 'none',
display: 'flex' as 'flex',
},
left: {
borderRightStyle: 'dotted' as 'dotted',
borderRadius: `${theme.shape.borderRadius}px 0 0 ${theme.shape.borderRadius}px`,
paddingTop: theme.spacing(2),
flex: 3,
overflow: 'hidden' as 'hidden',
backgroundColor: theme.palette.background.default,
color: theme.palette.text.primary,
overflowY: 'auto' as 'auto',
},
right: {
borderRadius: `0 ${theme.shape.borderRadius}px ${theme.shape.borderRadius}px 0`,
backgroundColor: theme.palette.background.paper,
padding: theme.spacing(2),
flex: 10,
},
connectionUri: {
width: '27em',
textOverflow: 'ellipsis' as 'ellipsis',
whiteSpace: 'nowrap' as 'nowrap',
overflow: 'hidden' as 'hidden',
color: theme.palette.text.secondary,
fontSize: '0.9em',
marginLeft: theme.spacing(4),
},
})
const mapStateToProps = (state: AppState) => {
return {
visible: !state.connection.connected,
showAdvancedSettings: state.connectionManager.showAdvancedSettings,
showCertificateSettings: state.connectionManager.showCertificateSettings,
connection: state.connectionManager.selected
? state.connectionManager.connections[state.connectionManager.selected]
: undefined,
}
}
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(connectionManagerActions, dispatch),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ConnectionSetup) as any)
@@ -0,0 +1,26 @@
import * as React from 'react'
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: {
height: theme.spacing(4),
width: theme.spacing(4),
minHeight: '0',
},
addIcon: {
height: theme.spacing(2),
},
})
export const AddButton = withStyles(styles)((props: { classes: any; action: any }) => {
return (
<span id="addProfileButton" style={{ marginRight: '12px' }}>
<Fab size="small" color="secondary" aria-label="Add" className={props.classes.addButton} onClick={props.action}>
<Add className={props.classes.addIcon} />
</Fab>
</span>
)
})
@@ -0,0 +1,71 @@
import React, { useCallback } from 'react'
import { connect } from 'react-redux'
import { ListItem, Typography } from '@mui/material'
import { toMqttConnection, ConnectionOptions } from '../../../model/ConnectionOptions'
import { withStyles } from '@mui/styles'
import { Theme } from '@mui/material/styles'
import { bindActionCreators } from 'redux'
import { connectionActions, connectionManagerActions } from '../../../actions'
export interface Props {
connection: ConnectionOptions
actions: {
connection: any
connectionManager: any
}
selected: boolean
classes: any
}
const ConnectionItem = (props: Props) => {
const connect = useCallback(() => {
const mqttOptions = toMqttConnection(props.connection)
if (mqttOptions) {
props.actions.connection.connect(mqttOptions, props.connection.id)
}
}, [props.connection, props])
const connection = props.connection.host && toMqttConnection(props.connection)
return (
<ListItem
button={true}
selected={props.selected}
style={{ display: 'block' }}
onClick={() => props.actions.connectionManager.selectConnection(props.connection.id)}
onDoubleClick={() => {
props.actions.connectionManager.selectConnection(props.connection.id)
connect()
}}
>
<Typography className={props.classes.name}>{props.connection.name || 'mqtt broker'}</Typography>
<Typography className={props.classes.details}>{connection && connection.url}</Typography>
</ListItem>
)
}
export const mapDispatchToProps = (dispatch: any) => {
return {
actions: {
connection: bindActionCreators(connectionActions, dispatch),
connectionManager: bindActionCreators(connectionManagerActions, dispatch),
},
}
}
export const connectionItemStyle = (theme: Theme) => ({
name: {
width: '100%',
textOverflow: 'ellipsis' as 'ellipsis',
whiteSpace: 'nowrap' as 'nowrap',
overflow: 'hidden' as 'hidden',
},
details: {
width: '100%',
textOverflow: 'ellipsis' as 'ellipsis',
whiteSpace: 'nowrap' as 'nowrap',
overflow: 'hidden' as 'hidden',
color: theme.palette.text.secondary,
fontSize: '0.7em',
},
})
export default connect(null, mapDispatchToProps)(withStyles(connectionItemStyle)(ConnectionItem) as any)
@@ -0,0 +1,82 @@
import ConnectionItem from './ConnectionItem'
const ConnectionItemAny = ConnectionItem as any
import React from 'react'
import { AddButton } from './AddButton'
import { AppState } from '../../../reducers'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import { connectionManagerActions } from '../../../actions'
import { ConnectionOptions } from '../../../model/ConnectionOptions'
import { KeyCodes } from '../../../utils/KeyCodes'
import { List } from '@mui/material'
import { Theme } from '@mui/material/styles'
import { withStyles } from '@mui/styles'
import { useGlobalKeyEventHandler } from '../../../effects/useGlobalKeyEventHandler'
interface Props {
classes: any
selected?: string
connections: { [s: string]: ConnectionOptions }
actions: typeof connectionManagerActions
}
function ProfileList(props: Props) {
const { actions, classes, connections, selected } = props
const selectConnection = (dir: 'next' | 'previous') => (event: KeyboardEvent) => {
if (!selected) {
return
}
const indexDirection = dir === 'next' ? 1 : -1
const connectionArray = Object.values(connections)
const selectedIndex = connectionArray.map(connection => connection.id).indexOf(selected)
const nextConnection = connectionArray[selectedIndex + indexDirection]
if (nextConnection) {
actions.selectConnection(nextConnection.id)
}
event.preventDefault()
}
useGlobalKeyEventHandler(KeyCodes.arrow_down, selectConnection('next'))
useGlobalKeyEventHandler(KeyCodes.arrow_up, selectConnection('previous'))
const createConnectionButton = (
<div style={{ padding: '8px 16px' }}>
<AddButton action={actions.createConnection} />
Connections
</div>
)
return (
<List style={{ height: '100%' }} component="nav" subheader={createConnectionButton}>
<div className={classes.list}>
{Object.values(connections).map(connection => (
<ConnectionItemAny connection={connection} key={connection.id} selected={selected === connection.id} />
))}
</div>
</List>
)
}
const styles = (theme: Theme) => ({
list: {
marginTop: theme.spacing(1),
height: `calc(100% - ${theme.spacing(6)})`,
overflowY: 'auto' as 'auto',
},
})
const mapDispatchToProps = (dispatch: any) => {
return {
actions: bindActionCreators(connectionManagerActions, dispatch),
}
}
const mapStateToProps = (state: AppState) => {
return {
connections: state.connectionManager.connections,
selected: state.connectionManager.selected,
}
}
export default connect(mapStateToProps, mapDispatchToProps)(withStyles(styles)(ProfileList) as any)
@@ -0,0 +1,90 @@
import React, { useCallback, useState } from 'react'
import Delete from '@mui/icons-material/Delete'
import { connectionManagerActions } from '../../actions'
import { ConnectionOptions } from '../../model/ConnectionOptions'
import {
IconButton,
TableContainer,
Table,
TableHead,
TableRow,
TableCell,
TableBody,
Paper,
Theme,
} from '@mui/material'
import { bindActionCreators } from 'redux'
import { withStyles } from '@mui/styles'
import { connect } from 'react-redux'
function Subscriptions(props: {
classes: any
connection: ConnectionOptions
managerActions: typeof connectionManagerActions
}) {
const { classes, connection, managerActions } = props
return (
<TableContainer component={Paper} className={`${classes.topicList} advanced-connection-settings-topic-list`}>
<Table size="small">
<TableHead>
<TableRow>
<TableCell align="left" padding="checkbox" className={classes.tableTitleCell}></TableCell>
<TableCell className={classes.tableTitleCell}>Topic</TableCell>
<TableCell align="right" className={classes.tableTitleCell}>
QoS
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{connection.subscriptions.map(subscription => (
<TableRow key={subscription.topic + '_qos_' + subscription.qos}>
<TableCell align="right" className={classes.tableCell}>
<IconButton
onClick={() => managerActions.deleteSubscription(subscription, connection.id)}
style={{ padding: '6px' }}
>
<Delete />
</IconButton>
</TableCell>
<TableCell component="th" scope="row" className={classes.tableCell}>
{subscription.topic}
</TableCell>
<TableCell align="right" className={classes.tableCell}>
{subscription.qos}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)
}
const mapDispatchToProps = (dispatch: any) => {
return {
managerActions: bindActionCreators(connectionManagerActions, dispatch),
}
}
const styles = (theme: Theme) => ({
tableCell: {
paddingTop: 0,
paddingBottom: 0,
wordBbreak: 'break-word',
},
tableTitleCell: {
paddingTop: `${theme.spacing(0.5)}px`,
paddingBottom: `${theme.spacing(0.5)}px`,
},
topicList: {
height: '196px',
overflowY: 'scroll' as 'scroll',
margin: `${theme.spacing(1)}px ${theme.spacing(1)}px 0 ${theme.spacing(1)}px`,
backgroundColor: theme.palette.background.default,
width: 'auto',
},
})
export default connect(undefined, mapDispatchToProps)(withStyles(styles)(Subscriptions) as any)
@@ -0,0 +1,12 @@
import React from 'react'
import { FormControlLabel, Switch } from '@mui/material'
export function ToggleSwitch(props: { value: boolean; classes: any; toggle: () => void; label: string }) {
const { classes, value, toggle, label } = props
const toggleSwitch = <Switch checked={value} onChange={toggle} color="primary" />
return (
<div className={classes.switch}>
<FormControlLabel control={toggleSwitch} label={label} labelPlacement="bottom" />
</div>
)
}
-70
View File
@@ -1,70 +0,0 @@
import * as React from 'react'
import { Snackbar, SnackbarContent } from '@material-ui/core'
import FileCopy from '@material-ui/icons/FileCopy'
import Check from '@material-ui/icons/Check'
import green from '@material-ui/core/colors/green'
import { withStyles, Theme } from '@material-ui/core/styles'
const copy = require('copy-text-to-clipboard')
interface Props {
value: string
classes: any
}
interface State {
didCopy: boolean
snackBarOpen: boolean
}
const styles = (theme: Theme) => ({
snackbar: {
backgroundColor: green[600],
color: theme.typography.button.color,
},
})
class Copy extends React.Component<Props, State> {
constructor(props: Props) {
super(props)
this.state = { didCopy: false, snackBarOpen: false }
}
public render() {
const icon = !this.state.didCopy
? <FileCopy fontSize="inherit" style={{ cursor: 'pointer' }} onClick={this.handleClick} />
: <Check fontSize="inherit" style={{ cursor: 'default' }} />
return <span>
<span style={{ fontSize: '16px' }}>{icon}</span>
<span>
<Snackbar
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
open={this.state.snackBarOpen}
autoHideDuration={2000}
onClose={() => { this.setState({ snackBarOpen: false }) }}
>
<SnackbarContent
className={this.props.classes.snackbar}
message="Copied to clipboard"
/>
</Snackbar>
</span>
</span>
}
private handleClick = (event: React.MouseEvent) => {
event.stopPropagation()
copy(this.props.value)
this.setState({ didCopy: true, snackBarOpen: true })
setTimeout(() => {
this.setState({ didCopy: false })
}, 1500)
}
}
export default withStyles(styles)(Copy)
+44
View File
@@ -0,0 +1,44 @@
import * as React from 'react'
import { Theme } from '@mui/material/styles'
import { withStyles } from '@mui/styles'
interface Props {
keyboardKey: string
classes: any
}
class Key extends React.Component<Props, {}> {
constructor(props: any) {
super(props)
this.state = { location: 'bottom' }
}
public render() {
return (
<div className={this.props.classes.keyStyle}>
<div className={this.props.classes.keyTextStyle}>{this.props.keyboardKey}</div>
</div>
)
}
}
const style = (theme: Theme) => ({
keyStyle: {
display: 'inline-block' as 'inline-block',
width: '1em',
height: '1em',
backgroundColor: '#bbb',
borderRadius: '10%',
verticalAlign: 'middle' as 'middle',
textAlign: 'center' as 'center',
textShadow: '1px 1px rgba(255,255,255,0.45)',
boxShadow: '0.08em 0.15em 0.01em 0px rgba(100,100,100,0.75)',
},
keyTextStyle: {
marginTop: '0.65em',
fontSize: '0.4em',
fontWeight: 'bold' as 'bold',
},
})
export default withStyles(style)(Key)
+83
View File
@@ -0,0 +1,83 @@
import * as React from 'react'
import { Theme } from '@mui/material/styles'
import { withStyles } from '@mui/styles'
const cursor = require('./cursor.png')
interface State {
enabled: boolean
target: { x: number; y: number }
position: { x: number; y: number }
stepSizeX: number
stepSizeY: number
}
class Demo extends React.Component<{ classes: any }, State> {
private timer: any
private frameInterval = 20
constructor(props: any) {
super(props)
this.state = { enabled: false, target: { x: 0, y: 0 }, position: { x: 0, y: 0 }, stepSizeX: 1, stepSizeY: 1 }
}
private moveCloser(steps: number = 0) {
const steSizeX = Math.min(this.state.stepSizeX, Math.abs(this.state.position.x - this.state.target.x))
const steSizeY = Math.min(this.state.stepSizeY, Math.abs(this.state.position.y - this.state.target.y))
const dirX = this.state.position.x > this.state.target.x ? -1 : 1
const dirY = this.state.position.y > this.state.target.y ? -1 : 1
if (steSizeX <= 0.1 && steSizeY <= 0.1) {
this.timer && clearTimeout(this.timer)
return
}
this.setState({
position: {
x: this.state.position.x + dirX * steSizeX,
y: this.state.position.y + dirY * steSizeY,
},
})
this.timer = setTimeout(() => {
this.moveCloser(steps + 1)
}, this.frameInterval)
}
public componentDidMount() {
;(window as any).demo.enableMouse = () => {
this.setState({ enabled: true })
}
;(window as any).demo.moveMouse = (x: number, y: number, animationTime: number) => {
const stepSizeX = Math.abs(this.state.position.x - x) / (animationTime / this.frameInterval)
const stepSizeY = Math.abs(this.state.position.y - y) / (animationTime / this.frameInterval)
this.setState({ stepSizeX, stepSizeY, enabled: true, target: { x, y } })
this.moveCloser()
}
}
public render() {
if (!this.state.enabled) {
return null
}
const cursorStyle = {
left: this.state.position.x + 2,
top: this.state.position.y + 2,
}
return <img src={cursor} style={cursorStyle} className={this.props.classes.cursor} />
}
}
const style = (theme: Theme) => ({
cursor: {
width: '32px',
height: '32px',
position: 'fixed' as 'fixed',
zIndex: 1000000,
filter: theme.palette.mode === 'light' ? undefined : 'invert(100%)',
pointerEvents: 'none' as 'none',
},
})
export default withStyles(style)(Demo)
+94
View File
@@ -0,0 +1,94 @@
import * as React from 'react'
import { Theme } from '@mui/material/styles'
import { withStyles } from '@mui/styles'
import Key from './Key'
interface State {
message?: string
keys: Array<string>
location: string
}
class Demo extends React.Component<{ classes: any }, State> {
private timer: any
constructor(props: any) {
super(props)
this.state = { location: 'bottom', keys: [] }
}
private clearTimer() {
this.timer && clearTimeout(this.timer)
}
public componentDidMount() {
;(window as any).demo.showMessage = (
message: string,
location: string,
duration: number,
keys: Array<string> = []
) => {
this.clearTimer()
this.setState({ message, location, keys })
this.timer = setTimeout(() => this.setState({ message: undefined }), duration)
}
;(window as any).demo.hideMessage = () => {
this.clearTimer()
this.setState({ message: undefined })
}
}
public render() {
const positions: { [s: string]: number } = {
top: 0,
bottom: -65,
middle: -32,
}
const style = {
position: 'fixed' as 'fixed',
left: '5vw',
zIndex: 1000000,
margin: '30vw auto 50vw',
right: '5vw',
bottom: `${positions[this.state.location]}vh`,
}
const style2 = {
textAlign: 'center' as 'center',
fontSize: '4em',
color: 'white',
backgroundColor: 'rgba(0, 0, 0, 0.8)',
borderRadius: '16px',
}
if (!this.state.message) {
return null
}
let keys: Array<any> = []
if (this.state.keys.length > 0) {
keys = this.state.keys
.map(key => [<Key key={key} keyboardKey={key} />])
.reduce((prev, current) => {
return [prev, '+' as any, current]
})
}
return (
<div style={style}>
<div style={style2}>
<span>{this.state.message}</span>
{keys.length > 0 ? <div className={this.props.classes.keysStyle}>{keys}</div> : null}
</div>
</div>
)
}
}
const style = (theme: Theme) => ({
keysStyle: {
fontSize: '1em',
display: 'inline-block' as 'inline-block',
transform: 'translateY(0.3em) translateX(0.8em)',
},
})
export default withStyles(style)(Demo)

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

+26
View File
@@ -0,0 +1,26 @@
import * as React from 'react'
import ShowText from './ShowText'
import Mouse from './Mouse'
let heapdump: any
function writeHeapdump(path?: string) {
if (!heapdump) {
//<heapdump = require('heapdump')
}
heapdump.writeSnapshot(path || `${Date.now()}.heapsnapshot`)
return path
}
;(window as any).demo = {
writeHeapdump,
}
export default function render(props: any) {
return (
<span>
<ShowText />
<Mouse />
</span>
)
}
@@ -1,17 +1,10 @@
import * as React from 'react'
import { electronRendererTelementry } from 'electron-telemetry'
import {
Button,
Modal,
Paper,
Toolbar,
Typography,
} from '@material-ui/core'
import Warning from '@material-ui/icons/Warning'
import SentimentDissatisfied from '@material-ui/icons/SentimentDissatisfied'
import { Theme, withStyles } from '@material-ui/core/styles'
import PersistentStorage from '../utils/PersistentStorage'
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
@@ -19,30 +12,30 @@ interface State {
interface Props {
classes: any
children?: React.ReactNode
}
class ErrorBoundary extends React.Component<Props, State> {
class ErrorBoundary extends React.PureComponent<Props, State> {
public static getDerivedStateFromError(error: Error) {
return { error }
}
constructor(props: Props) {
super(props)
this.state = {}
}
public componentDidCatch(error: Error, errorInfo: any) {
electronRendererTelementry.trackError(error)
console.log('did catch', error)
}
public static getDerivedStateFromError(error: Error) {
return { error }
}
private restart = () => {
window.location = window.location
window.location.reload()
}
private clearStorage = () => {
localStorage.clear()
window.location = window.location
PersistentStorage.clear()
window.location.reload()
}
public componentDidCatch(error: Error, errorInfo: any) {
// electronRendererTelemetry.trackError(error)
console.log('did catch', error)
}
public render() {
@@ -55,23 +48,36 @@ class ErrorBoundary extends React.Component<Props, State> {
<Modal open={true} disableAutoFocus={true}>
<Paper className={classes.root}>
<Toolbar style={{ padding: '0' }}>
<Typography className={classes.title} variant="h6" color="inherit"><Warning /> Oooooops!</Typography>
<Typography className={classes.title} variant="h6" color="inherit">
<Warning /> Oooooops!
</Typography>
</Toolbar>
<Typography className={classes.centered}>I hoped that you would never see this window, but MQTT-Explorer had an unexpected error.</Typography>
<Typography className={classes.centered}><SentimentDissatisfied /></Typography>
<Typography className={classes.centered}>
I hoped that you would never see this window, but MQTT-Explorer had an unexpected error.
</Typography>
<Typography className={classes.centered}>
<SentimentDissatisfied />
</Typography>
<pre className={classes.textColor} style={{ maxHeight: '35vh', overflow: 'scroll' }}>
<code className={classes.textColor}>
{this.state.error.stack}
</code>
<code className={classes.textColor}>{this.state.error.stack}</code>
</pre>
<Typography>
Please report this issue with a short description of what happened to
<span> <a className={classes.textColor} href="https://github.com/thomasnordquist/MQTT-Explorer/issues">https://github.com/thomasnordquist/MQTT-Explorer/issues</a></span>
<span>
{' '}
<a className={classes.textColor} href="https://github.com/thomasnordquist/MQTT-Explorer/issues">
https://github.com/thomasnordquist/MQTT-Explorer/issues
</a>
</span>
</Typography>
<div>
<div className={classes.buttonPositioning}>
<Button className={classes.button} variant="contained" color="secondary" onClick={this.clearStorage}>Start Fresh</Button>
<Button className={classes.button} variant="contained" color="primary" onClick={this.restart}>Restart</Button>
<Button className={classes.button} variant="contained" color="secondary" onClick={this.clearStorage}>
Start Fresh
</Button>
<Button className={classes.button} variant="contained" color="primary" onClick={this.restart}>
Restart
</Button>
</div>
</div>
</Paper>
@@ -89,7 +95,7 @@ const styles = (theme: Theme) => ({
maxWidth: 650,
backgroundColor: theme.palette.background.default,
margin: '10vh auto auto auto',
padding: `${2 * theme.spacing.unit}px`,
padding: theme.spacing(2),
outline: 'none',
},
title: {
@@ -99,13 +105,14 @@ const styles = (theme: Theme) => ({
},
textColor: {
color: theme.palette.text.primary,
userSelect: 'all' as 'all',
},
centered: {
textAlign: 'center' as 'center',
},
buttonPositioning: {
textAlign: 'center' as 'center',
marginTop: `${theme.spacing.unit * 2}px`,
marginTop: theme.spacing(2),
},
})
+129
View File
@@ -0,0 +1,129 @@
import * as React from 'react'
import ChartPanel from '../ChartPanel'
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 { 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
paneDefaults: any
connectionId?: string
chartPanelItems: List<ChartParameters>
}
function ContentView(props: Props) {
const [height, setHeight] = React.useState<string | number>('100%')
const [sidebarWidth, setSidebarWidth] = React.useState<string | number>('40%')
const [detectedHeight, setDetectedHeight] = React.useState(0)
const [detectedSidebarWidth, setDetectedSidebarWidth] = React.useState(0)
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: any) => {
setDetectedSidebarWidth(width)
}, [])
const closeDrawerCompletelyIfItSitsOnTheEdge = React.useCallback(() => {
if (detectedHeight < 30) {
setHeight('100%')
}
}, [detectedHeight])
const closeSidebarCompletelyIfItSitsOnTheEdge = React.useCallback(() => {
if (detectedSidebarWidth < 30) {
setSidebarWidth('0%')
}
}, [detectedSidebarWidth])
// Open chart panel on start and when a new chart is added but the panel is closed
React.useEffect(() => {
const almostClosed = !isNaN(height as any) && detectedHeight < 30
if ((!height || height === '100%' || almostClosed) && props.chartPanelItems.count() > 0) {
setHeight('calc(100% - 250px)')
}
if (props.chartPanelItems.count() === 0) {
setHeight('100%')
}
}, [props.chartPanelItems])
return (
<div className={props.paneDefaults}>
<span>
<ReactSplitPane
step={20}
primary="second"
className={props.heightProperty}
split="vertical"
minSize={0}
size={sidebarWidth}
onChange={(size: number) => setSidebarWidth(size)}
onDragFinished={closeSidebarCompletelyIfItSitsOnTheEdge}
allowResize={true}
style={{ height: '100%' }}
pane1Style={{ overflowX: 'hidden' }}
resizerStyle={{ height: '100%' }}
>
<span>
<ReactSplitPane
step={10}
split="horizontal"
minSize={0}
size={height}
allowResize={true}
style={{ height: 'calc(100vh - 64px)' }}
pane1Style={{ maxHeight: '100%' }}
pane2Style={{ borderTop: '1px solid #999', display: 'flex' }}
onChange={(size: number) => setHeight(size)}
onDragFinished={closeDrawerCompletelyIfItSitsOnTheEdge}
>
<Tree />
{/** Passing height constraints via flex options down */}
<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 */}
<ChartPanel />
</div>
</ReactSplitPane>
</span>
<div ref={widthRef} style={{ height: '100%' }}>
<div
className={props.paneDefaults}
style={{ minWidth: '250px', height: '100%', overflowY: 'auto', overflowX: 'hidden' }}
>
<Sidebar connectionId={props.connectionId} />
</div>
</div>
</ReactSplitPane>
</span>
</div>
)
}
const mapStateToProps = (state: AppState) => {
return {
chartPanelItems: state.charts.get('charts'),
}
}
export default connect(mapStateToProps)(ContentView)
@@ -1,27 +1,29 @@
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
type: 'error' | 'notification'
onClose: () => void
classes: any
}
class Notification extends React.Component<Props, {}> {
class Notification extends React.PureComponent<Props, {}> {
constructor(props: Props) {
super(props)
}
public static styles = (theme: Theme) => ({
success: {
notification: {
backgroundColor: green[600],
color: theme.typography.button.color,
},
error: {
backgroundColor: red[600],
backgroundColor: theme.palette.error.main,
color: theme.typography.button.color,
},
})
@@ -36,11 +38,11 @@ class Notification extends React.Component<Props, {}> {
<Snackbar
anchorOrigin={snackbarAnchor}
open={Boolean(this.props.message)}
autoHideDuration={10000}
autoHideDuration={this.props.type === 'error' ? 10000 : 2000}
onClose={this.props.onClose}
>
<SnackbarContent
className={this.props.classes.error}
className={this.props.type === 'error' ? this.props.classes.error : this.props.classes.notification}
message={this.props.message}
/>
</Snackbar>

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