mirror of
https://github.com/thomasnordquist/MQTT-Explorer.git
synced 2026-09-12 01:23:31 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
be83c3f2cc | ||
|
|
1a8a9224b4 | ||
|
|
651246ff83 | ||
|
|
73b8e6ff9f |
@@ -1,64 +0,0 @@
|
||||
# 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
|
||||
@@ -1,72 +1,5 @@
|
||||
# GitHub Copilot Agent Instructions for MQTT Explorer
|
||||
|
||||
## Style Guide
|
||||
|
||||
**Always follow the [Style Guide](../STYLE_GUIDE.md)** when making code changes. The style guide covers:
|
||||
- Code formatting (Prettier + TSLint)
|
||||
- TypeScript conventions
|
||||
- React component patterns
|
||||
- State management (Redux)
|
||||
- File organization and naming
|
||||
- Testing practices
|
||||
- Security guidelines
|
||||
|
||||
## 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
|
||||
|
||||
@@ -1,219 +0,0 @@
|
||||
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
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
- name: Test
|
||||
run: yarn test
|
||||
|
||||
electron-tests:
|
||||
ui-tests:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ghcr.io/thomasnordquist/mqtt-explorer-ui-tests:latest
|
||||
@@ -36,14 +36,14 @@ jobs:
|
||||
run: yarn install --frozen-lockfile
|
||||
- name: Build
|
||||
run: yarn build
|
||||
- name: Run Electron UI Tests
|
||||
- name: Run 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
|
||||
name: ui-test-screenshots
|
||||
path: |
|
||||
test-screenshot-*.png
|
||||
retention-days: 30
|
||||
|
||||
@@ -10,54 +10,6 @@ MQTT Explorer uses GitHub Actions for continuous integration and testing. The pi
|
||||
|
||||
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
|
||||
@@ -140,35 +92,6 @@ Example:
|
||||
|
||||
## 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
|
||||
|
||||
@@ -1,250 +0,0 @@
|
||||
# MQTT Explorer - Docker Browser Mode
|
||||
|
||||
Docker image for running MQTT Explorer in browser mode.
|
||||
|
||||
## Try It Now
|
||||
|
||||
[](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:
|
||||
|
||||
[](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
|
||||
|
||||
[](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
|
||||
|
||||
[](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.
|
||||
@@ -1,81 +0,0 @@
|
||||
# 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"]
|
||||
@@ -54,27 +54,6 @@ yarn start:server
|
||||
|
||||
Then open your browser to `http://localhost:3000`. For more details, see [BROWSER_MODE.md](BROWSER_MODE.md).
|
||||
|
||||
### Docker (Browser Mode)
|
||||
|
||||
[](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
|
||||
@@ -99,55 +78,14 @@ 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.
|
||||
|
||||
For coding standards, design patterns, and best practices, see the [Style Guide](STYLE_GUIDE.md).
|
||||
|
||||
## Automated Tests
|
||||
|
||||
MQTT Explorer uses multiple test suites to ensure reliability and quality:
|
||||
To achieve a reliable product automated tests run regularly on CI.
|
||||
|
||||
### 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
|
||||
```
|
||||
- **Data model tests**: `yarn test:backend`
|
||||
- **App tests**: `yarn test:app`
|
||||
- **UI test suite**: `yarn test:ui` (independent, deterministic tests)
|
||||
- **Demo video**: `yarn ui-test` (UI test recording for documentation)
|
||||
|
||||
### Run UI Test Suite
|
||||
|
||||
@@ -166,27 +104,13 @@ 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:
|
||||
A [mosquitto](https://mosquitto.org/) MQTT broker is required to generate the demo video.
|
||||
|
||||
```bash
|
||||
yarn build
|
||||
yarn test:demo-video
|
||||
yarn ui-test
|
||||
```
|
||||
|
||||
**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.
|
||||
|
||||
-183
@@ -1,183 +0,0 @@
|
||||
# MQTT Explorer Style Guide
|
||||
|
||||
Coding standards and patterns for the MQTT Explorer project, optimized for coding agents.
|
||||
|
||||
## Code Formatting
|
||||
|
||||
**Prettier config:** No semicolons, single quotes, 2 spaces, 120 char max, ES5 trailing commas
|
||||
**TSLint:** Airbnb base, generic arrays `Array<T>`, explicit access modifiers (`public`/`private`/`protected`)
|
||||
|
||||
```typescript
|
||||
// Format example
|
||||
const name = 'MQTT Explorer'
|
||||
const items: Array<string> = []
|
||||
|
||||
class Example {
|
||||
private client: MqttClient
|
||||
public connect(options: MqttOptions) {}
|
||||
}
|
||||
```
|
||||
|
||||
**Commands:** `yarn lint`, `yarn lint:fix`
|
||||
|
||||
## TypeScript
|
||||
|
||||
- Strict mode enabled (`noImplicitAny`, `strictNullChecks`)
|
||||
- Explicit function types: `function name(param: Type): ReturnType {}`
|
||||
- Avoid `any`, use interfaces for objects, enums for constants
|
||||
- Type imports: `import type { Type } from './module'`
|
||||
|
||||
```typescript
|
||||
enum ActionTypes {
|
||||
CONNECTION_SET_CONNECTING = 'CONNECTION_SET_CONNECTING',
|
||||
}
|
||||
|
||||
interface ConnectionState {
|
||||
connected: boolean
|
||||
error?: string
|
||||
}
|
||||
```
|
||||
|
||||
## React Components
|
||||
|
||||
**Class components:** Use `React.PureComponent`, type props/state, mark methods `public`/`private`
|
||||
|
||||
```typescript
|
||||
interface Props {
|
||||
connectionId: string
|
||||
actions: typeof globalActions
|
||||
}
|
||||
|
||||
class App extends React.PureComponent<Props, {}> {
|
||||
public render() {
|
||||
return <div />
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Functional components:** Destructure props, use custom hooks, keep under 200 lines
|
||||
|
||||
```typescript
|
||||
function Sidebar(props: Props) {
|
||||
const { tree, nodePath } = props
|
||||
const node = usePollingToFetchTreeNode(tree, nodePath)
|
||||
return <div />
|
||||
}
|
||||
```
|
||||
|
||||
**Hooks:** Prefix with `use`, place in `hooks/` or `helper/`, always specify `useEffect` dependencies
|
||||
|
||||
**Lazy loading:** `const Component = React.lazy(() => import('./Component'))`
|
||||
|
||||
**Material-UI:** Use both `ThemeProvider` and `LegacyThemeProvider`, `withStyles` for class components
|
||||
|
||||
## State Management (Redux)
|
||||
|
||||
**Reducers:** Define state interface, use enums for action types, use `createReducer` helper, return new objects
|
||||
|
||||
```typescript
|
||||
export const reducer = createReducer<State>(initialState, {
|
||||
[ActionTypes.ACTION]: (state, action) => ({ ...state, field: value }),
|
||||
})
|
||||
```
|
||||
|
||||
**Actions:** Use thunk for async, export action creators
|
||||
|
||||
```typescript
|
||||
export const connect = (options: MqttOptions, id: string) =>
|
||||
(dispatch: Dispatch<any>, getState: () => AppState) => {
|
||||
dispatch(connecting(id))
|
||||
}
|
||||
```
|
||||
|
||||
**Connect:** `mapStateToProps`, `mapDispatchToProps` with `bindActionCreators`
|
||||
|
||||
## File Organization
|
||||
|
||||
```
|
||||
app/src/
|
||||
actions/ # Redux actions
|
||||
components/ # React components (PascalCase folders)
|
||||
reducers/ # Redux reducers
|
||||
hooks/ # Custom hooks
|
||||
model/ # Frontend models
|
||||
utils/ # Utilities
|
||||
|
||||
backend/src/
|
||||
Model/ # Core data models
|
||||
DataSource/ # MQTT sources
|
||||
|
||||
src/
|
||||
electron.ts # Electron main
|
||||
server.ts # Express server
|
||||
```
|
||||
|
||||
**Component structure:**
|
||||
```
|
||||
ComponentName/
|
||||
index.ts # Re-exports
|
||||
ComponentName.tsx
|
||||
SubComponent.tsx
|
||||
```
|
||||
|
||||
**File naming:** Components = PascalCase, utilities = camelCase, tests = `*.spec.ts`, hooks = `useHookName.tsx`
|
||||
|
||||
## Naming Conventions
|
||||
|
||||
- Variables/functions: `camelCase`
|
||||
- Classes/interfaces: `PascalCase`
|
||||
- Constants: `SCREAMING_SNAKE_CASE`
|
||||
- Booleans: `isX`, `hasX`, `shouldX`, `canX`
|
||||
- Props/State interfaces: `Props`, `State`
|
||||
|
||||
## Testing
|
||||
|
||||
**Framework:** Mocha + Chai
|
||||
**Location:** `app/test/`, `backend/src/spec/`, `src/spec/`, co-located `*.spec.tsx`
|
||||
|
||||
```typescript
|
||||
describe('Component', () => {
|
||||
it('does something', () => {
|
||||
expect(result).to.eq(expected)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
**Commands:** `yarn test` (all), `yarn test:app`, `yarn test:backend`, `yarn test:ui` (needs build)
|
||||
|
||||
## Security
|
||||
|
||||
- Never hardcode credentials, use `process.env`
|
||||
- Validate/sanitize all user input
|
||||
- Use helmet.js, rate limiting, bcrypt, HTTPS in production
|
||||
- See [SECURITY.md](SECURITY.md)
|
||||
|
||||
## Documentation
|
||||
|
||||
- JSDoc for public APIs
|
||||
- Comments only for complex logic, workarounds, TODOs
|
||||
- No obvious/redundant comments
|
||||
|
||||
## Development
|
||||
|
||||
**Workflow:**
|
||||
- `yarn dev` - Electron dev mode
|
||||
- `yarn dev:server` - Browser mode with hot reload (port 8080)
|
||||
- `yarn build` / `yarn build:server`
|
||||
- `yarn lint` / `yarn lint:fix`
|
||||
|
||||
**Git:** Feature branches from `master`, conventional commits (`feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:`)
|
||||
|
||||
**Performance:** Use `PureComponent`, throttle/debounce, `React.lazy`, `useCallback`/`useMemo`, avoid inline functions in render
|
||||
|
||||
## Architecture
|
||||
|
||||
**Event system:** `rendererEvents.emit()`, `rendererEvents.subscribe()`, `rendererEvents.unsubscribe()`
|
||||
|
||||
**Data flow:** User Action → Redux → Event System → Backend → MQTT → State Update → Re-render
|
||||
|
||||
**MVVM:** Model (`backend/src/Model/`), View (components), ViewModel (`app/src/model/`)
|
||||
|
||||
---
|
||||
|
||||
**Key principle:** Prioritize clarity and maintainability. When in doubt, follow existing code patterns.
|
||||
+9
-9
@@ -16,10 +16,10 @@
|
||||
"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",
|
||||
"@emotion/styled": "^11.14.0",
|
||||
"@mui/icons-material": "^5.18.0",
|
||||
"@mui/lab": "^5.0.0-alpha.177",
|
||||
"@mui/material": "^5.18.0",
|
||||
"@mui/styles": "^6.4.8",
|
||||
"@types/react-transition-group": "^4.4.11",
|
||||
"ace-builds": "^1.4.11",
|
||||
@@ -44,9 +44,9 @@
|
||||
"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": "^18.3.1",
|
||||
"react-ace": "^12.0.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-redux": "^9.2.0",
|
||||
"react-resize-detector": "^11.0.1",
|
||||
"react-split-pane": "^0.1.92",
|
||||
@@ -68,8 +68,8 @@
|
||||
"@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": "^18.3.18",
|
||||
"@types/react-dom": "^18.3.5",
|
||||
"@types/react-redux": "^7.1.34",
|
||||
"@types/react-resize-detector": "^4.0.3",
|
||||
"@types/sha1": "^1.1.1",
|
||||
|
||||
@@ -147,6 +147,12 @@ export const toggleCertificateSettings = (): Action => ({
|
||||
type: ActionTypes.CONNECTION_MANAGER_TOGGLE_CERTIFICATE_SETTINGS,
|
||||
})
|
||||
|
||||
export const moveConnection = (connectionId: string, direction: 'up' | 'down'): Action => ({
|
||||
connectionId,
|
||||
direction,
|
||||
type: ActionTypes.CONNECTION_MANAGER_MOVE_CONNECTION,
|
||||
})
|
||||
|
||||
export const deleteConnection = (connectionId: string) => (dispatch: Dispatch<any>, getState: () => AppState) => {
|
||||
const connectionIds = Object.keys(getState().connectionManager.connections)
|
||||
const connectionIdLocation = connectionIds.indexOf(connectionId)
|
||||
|
||||
@@ -68,6 +68,17 @@ let migrations: Migration[] = [
|
||||
}
|
||||
},
|
||||
},
|
||||
// Add order field for connection ordering
|
||||
{
|
||||
from: 1,
|
||||
apply: (connection: ConnectionOptions): ConnectionOptions => {
|
||||
if (connection.order !== undefined) {
|
||||
return connection
|
||||
}
|
||||
// Order will be assigned during migration based on current position
|
||||
return connection
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const connectionMigrator = new ConfigMigrator(migrations)
|
||||
@@ -80,8 +91,14 @@ function isMigrationNecessary(connections: ConnectionDictionary): boolean {
|
||||
|
||||
function applyMigrations(connections: ConnectionDictionary): ConnectionDictionary {
|
||||
let newConnectionDictionary: ConnectionDictionary = {}
|
||||
Object.keys(connections).forEach(key => {
|
||||
const connectionKeys = Object.keys(connections)
|
||||
|
||||
connectionKeys.forEach((key, index) => {
|
||||
let newConnection = connectionMigrator.applyMigrations(connections[key]) as any
|
||||
// If the migration didn't assign an order, assign one based on current position
|
||||
if (newConnection.order === undefined) {
|
||||
newConnection.order = index
|
||||
}
|
||||
newConnectionDictionary[newConnection.id] = newConnection
|
||||
})
|
||||
|
||||
|
||||
@@ -60,18 +60,6 @@ socket.on('connect', () => {
|
||||
}
|
||||
})
|
||||
|
||||
// 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
|
||||
|
||||
@@ -2,7 +2,6 @@ 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
|
||||
@@ -11,48 +10,17 @@ interface BrowserAuthWrapperProps {
|
||||
export function BrowserAuthWrapper(props: BrowserAuthWrapperProps) {
|
||||
const [isAuthenticated, setIsAuthenticated] = React.useState(false)
|
||||
const [loginError, setLoginError] = React.useState<string | undefined>()
|
||||
const [showLogin, setShowLogin] = React.useState(false)
|
||||
const [showLogin, setShowLogin] = React.useState(isBrowserMode) // Show login initially in browser mode
|
||||
const [waitTimeSeconds, setWaitTimeSeconds] = React.useState<number | undefined>()
|
||||
const [isConnecting, setIsConnecting] = React.useState(false)
|
||||
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')
|
||||
@@ -98,15 +66,23 @@ export function BrowserAuthWrapper(props: BrowserAuthWrapperProps) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
// Check if already authenticated
|
||||
const username = sessionStorage.getItem('mqtt-explorer-username')
|
||||
const password = sessionStorage.getItem('mqtt-explorer-password')
|
||||
|
||||
if (username && password) {
|
||||
// Credentials exist, try to connect with them
|
||||
setIsConnecting(true)
|
||||
connectSocket()
|
||||
} else {
|
||||
// No credentials, show login dialog
|
||||
setShowLogin(true)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -133,14 +109,9 @@ export function BrowserAuthWrapper(props: BrowserAuthWrapperProps) {
|
||||
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>
|
||||
return <>{props.children}</>
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
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'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import { connect } from 'react-redux'
|
||||
import { ListItem, Typography } from '@mui/material'
|
||||
import { ListItem, Typography, Box } from '@mui/material'
|
||||
import { DragIndicator } from '@mui/icons-material'
|
||||
import { toMqttConnection, ConnectionOptions } from '../../../model/ConnectionOptions'
|
||||
import { withStyles } from '@mui/styles'
|
||||
import { Theme } from '@mui/material/styles'
|
||||
@@ -15,6 +16,9 @@ export interface Props {
|
||||
}
|
||||
selected: boolean
|
||||
classes: any
|
||||
onDragStart: (connectionId: string) => void
|
||||
onDragOver: (e: React.DragEvent) => void
|
||||
onDrop: (connectionId: string) => void
|
||||
}
|
||||
|
||||
const ConnectionItem = (props: Props) => {
|
||||
@@ -25,20 +29,48 @@ const ConnectionItem = (props: Props) => {
|
||||
}
|
||||
}, [props.connection, props])
|
||||
|
||||
const handleDragStart = (e: React.DragEvent) => {
|
||||
e.stopPropagation()
|
||||
props.onDragStart(props.connection.id)
|
||||
}
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
props.onDragOver(e)
|
||||
}
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
props.onDrop(props.connection.id)
|
||||
}
|
||||
|
||||
const connection = props.connection.host && toMqttConnection(props.connection)
|
||||
return (
|
||||
<ListItem
|
||||
button={true}
|
||||
selected={props.selected}
|
||||
style={{ display: 'block' }}
|
||||
className={props.classes.itemContainer}
|
||||
onClick={() => props.actions.connectionManager.selectConnection(props.connection.id)}
|
||||
onDoubleClick={() => {
|
||||
props.actions.connectionManager.selectConnection(props.connection.id)
|
||||
connect()
|
||||
}}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleDrop}
|
||||
>
|
||||
<Typography className={props.classes.name}>{props.connection.name || 'mqtt broker'}</Typography>
|
||||
<Typography className={props.classes.details}>{connection && connection.url}</Typography>
|
||||
<Box
|
||||
className={props.classes.dragHandle}
|
||||
draggable
|
||||
onDragStart={handleDragStart}
|
||||
>
|
||||
<DragIndicator fontSize="small" />
|
||||
</Box>
|
||||
<Box className={props.classes.textContainer}>
|
||||
<Typography className={props.classes.name}>{props.connection.name || 'mqtt broker'}</Typography>
|
||||
<Typography className={props.classes.details}>{connection && connection.url}</Typography>
|
||||
</Box>
|
||||
</ListItem>
|
||||
)
|
||||
}
|
||||
@@ -66,6 +98,25 @@ export const connectionItemStyle = (theme: Theme) => ({
|
||||
color: theme.palette.text.secondary,
|
||||
fontSize: '0.7em',
|
||||
},
|
||||
itemContainer: {
|
||||
display: 'flex' as 'flex',
|
||||
alignItems: 'center' as 'center',
|
||||
padding: '8px 8px 8px 8px',
|
||||
},
|
||||
textContainer: {
|
||||
flex: 1,
|
||||
overflow: 'hidden' as 'hidden',
|
||||
},
|
||||
dragHandle: {
|
||||
display: 'flex' as 'flex',
|
||||
alignItems: 'center' as 'center',
|
||||
marginRight: '8px',
|
||||
cursor: 'grab' as 'grab',
|
||||
color: theme.palette.text.secondary,
|
||||
'&:active': {
|
||||
cursor: 'grabbing' as 'grabbing',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
export default connect(null, mapDispatchToProps)(withStyles(connectionItemStyle)(ConnectionItem) as any)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import ConnectionItem from './ConnectionItem'
|
||||
const ConnectionItemAny = ConnectionItem as any
|
||||
import React from 'react'
|
||||
import React, { useState } from 'react'
|
||||
import { AddButton } from './AddButton'
|
||||
import { AppState } from '../../../reducers'
|
||||
import { bindActionCreators } from 'redux'
|
||||
@@ -22,13 +22,14 @@ interface Props {
|
||||
|
||||
function ProfileList(props: Props) {
|
||||
const { actions, classes, connections, selected } = props
|
||||
const [draggedConnectionId, setDraggedConnectionId] = useState<string | null>(null)
|
||||
|
||||
const selectConnection = (dir: 'next' | 'previous') => (event: KeyboardEvent) => {
|
||||
if (!selected) {
|
||||
return
|
||||
}
|
||||
const indexDirection = dir === 'next' ? 1 : -1
|
||||
const connectionArray = Object.values(connections)
|
||||
const connectionArray = Object.values(connections).sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
const selectedIndex = connectionArray.map(connection => connection.id).indexOf(selected)
|
||||
const nextConnection = connectionArray[selectedIndex + indexDirection]
|
||||
if (nextConnection) {
|
||||
@@ -40,6 +41,39 @@ function ProfileList(props: Props) {
|
||||
useGlobalKeyEventHandler(KeyCodes.arrow_down, selectConnection('next'))
|
||||
useGlobalKeyEventHandler(KeyCodes.arrow_up, selectConnection('previous'))
|
||||
|
||||
const handleDragStart = (connectionId: string) => {
|
||||
setDraggedConnectionId(connectionId)
|
||||
}
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
const handleDrop = (targetConnectionId: string) => {
|
||||
if (!draggedConnectionId || draggedConnectionId === targetConnectionId) {
|
||||
setDraggedConnectionId(null)
|
||||
return
|
||||
}
|
||||
|
||||
const sortedConnections = Object.values(connections).sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
const draggedIndex = sortedConnections.findIndex(c => c.id === draggedConnectionId)
|
||||
const targetIndex = sortedConnections.findIndex(c => c.id === targetConnectionId)
|
||||
|
||||
if (draggedIndex === -1 || targetIndex === -1) {
|
||||
setDraggedConnectionId(null)
|
||||
return
|
||||
}
|
||||
|
||||
// Swap order values
|
||||
const draggedConnection = sortedConnections[draggedIndex]
|
||||
const targetConnection = sortedConnections[targetIndex]
|
||||
|
||||
actions.updateConnection(draggedConnection.id, { order: targetConnection.order })
|
||||
actions.updateConnection(targetConnection.id, { order: draggedConnection.order })
|
||||
|
||||
setDraggedConnectionId(null)
|
||||
}
|
||||
|
||||
const createConnectionButton = (
|
||||
<div style={{ padding: '8px 16px' }}>
|
||||
<AddButton action={actions.createConnection} />
|
||||
@@ -50,9 +84,18 @@ function ProfileList(props: Props) {
|
||||
return (
|
||||
<List style={{ height: '100%' }} component="nav" subheader={createConnectionButton}>
|
||||
<div className={classes.list}>
|
||||
{Object.values(connections).map(connection => (
|
||||
<ConnectionItemAny connection={connection} key={connection.id} selected={selected === connection.id} />
|
||||
))}
|
||||
{Object.values(connections)
|
||||
.sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
.map(connection => (
|
||||
<ConnectionItemAny
|
||||
connection={connection}
|
||||
key={connection.id}
|
||||
selected={selected === connection.id}
|
||||
onDragStart={handleDragStart}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleDrop}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</List>
|
||||
)
|
||||
|
||||
@@ -14,7 +14,6 @@ import { connectionActions, globalActions, settingsActions } from '../../actions
|
||||
import { Theme } from '@mui/material/styles'
|
||||
import { withStyles } from '@mui/styles'
|
||||
import { isBrowserMode } from '../../utils/browserMode'
|
||||
import { useAuth } from '../../contexts/AuthContext'
|
||||
|
||||
const styles = (theme: Theme) => ({
|
||||
title: {
|
||||
@@ -105,7 +104,15 @@ class TitleBar extends React.PureComponent<Props, {}> {
|
||||
>
|
||||
Disconnect <CloudOff className={classes.disconnectIcon} />
|
||||
</Button>
|
||||
<LogoutButton classes={classes} onLogout={this.handleLogout} />
|
||||
{isBrowserMode && (
|
||||
<Button
|
||||
className={classes.logout}
|
||||
sx={{ color: 'primary.contrastText' }}
|
||||
onClick={this.handleLogout}
|
||||
>
|
||||
Logout <Logout className={classes.disconnectIcon} />
|
||||
</Button>
|
||||
)}
|
||||
<ConnectionHealthIndicatorAny withBackground={true} />
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
@@ -113,25 +120,6 @@ class TitleBar extends React.PureComponent<Props, {}> {
|
||||
}
|
||||
}
|
||||
|
||||
// Separate component to use hooks
|
||||
function LogoutButton({ classes, onLogout }: { classes: any; onLogout: () => void }) {
|
||||
const { authDisabled } = useAuth()
|
||||
|
||||
if (!isBrowserMode || authDisabled) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={classes.logout}
|
||||
sx={{ color: 'primary.contrastText' }}
|
||||
onClick={onLogout}
|
||||
>
|
||||
Logout <Logout className={classes.disconnectIcon} />
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: AppState) => {
|
||||
return {
|
||||
topicFilter: state.settings.get('topicFilter'),
|
||||
|
||||
@@ -41,29 +41,31 @@ function ChartPreview(props: Props) {
|
||||
|
||||
const addChartToPanelButton = hasEnoughDataToDisplayDiagrams ? (
|
||||
<Tooltip title="Add to chart panel">
|
||||
<span
|
||||
<ShowChart
|
||||
ref={chartIconRef}
|
||||
className={props.classes.icon}
|
||||
onMouseEnter={mouseOver}
|
||||
onMouseLeave={mouseOut}
|
||||
onClick={onClick}
|
||||
style={{ cursor: 'pointer', display: 'inline-flex' }}
|
||||
>
|
||||
<ShowChart className={props.classes.icon} />
|
||||
</span>
|
||||
data-test-type="ShowChart"
|
||||
data-test={props.literal.path}
|
||||
/>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip title="Add to chart panel, not enough data for preview">
|
||||
<span onClick={onClick} style={{ cursor: 'pointer', display: 'inline-flex' }}>
|
||||
<ShowChart className={props.classes.icon} style={{ color: '#aaa' }} />
|
||||
</span>
|
||||
<ShowChart
|
||||
onClick={onClick}
|
||||
className={props.classes.icon}
|
||||
style={{ color: '#aaa' }}
|
||||
data-test-type="ShowChart"
|
||||
data-test={props.literal.path}
|
||||
/>
|
||||
</Tooltip>
|
||||
)
|
||||
|
||||
return (
|
||||
<div style={{ display: 'inline' }}>
|
||||
<span data-test-type="ShowChart" data-test={props.literal.path} style={{ display: 'inline-block' }}>
|
||||
{addChartToPanelButton}
|
||||
</span>
|
||||
<span>
|
||||
{addChartToPanelButton}
|
||||
<Popper open={open} anchorEl={chartIconRef.current} placement="left-end">
|
||||
<Fade in={open} timeout={300}>
|
||||
<Paper style={{ width: '300px' }}>
|
||||
@@ -75,7 +77,7 @@ function ChartPreview(props: Props) {
|
||||
</Paper>
|
||||
</Fade>
|
||||
</Popper>
|
||||
</div>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useCallback } from 'react'
|
||||
import Code from '@mui/icons-material/Code'
|
||||
import Reorder from '@mui/icons-material/Reorder'
|
||||
import ToggleButton from '@mui/material/ToggleButton'
|
||||
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup'
|
||||
import ToggleButton from '@mui/lab/ToggleButton'
|
||||
import ToggleButtonGroup from '@mui/lab/ToggleButtonGroup'
|
||||
import { settingsActions } from '../../../actions'
|
||||
import { Tooltip } from '@mui/material'
|
||||
import { withStyles } from '@mui/styles'
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import * as React from 'react'
|
||||
|
||||
interface AuthContextType {
|
||||
authDisabled: boolean
|
||||
}
|
||||
|
||||
export const AuthContext = React.createContext<AuthContextType>({
|
||||
authDisabled: false,
|
||||
})
|
||||
|
||||
export function useAuth() {
|
||||
return React.useContext(AuthContext)
|
||||
}
|
||||
@@ -27,6 +27,7 @@ export interface ConnectionOptions {
|
||||
clientKey?: CertificateParameters
|
||||
clientId?: string
|
||||
subscriptions: Array<Subscription>
|
||||
order?: number
|
||||
}
|
||||
|
||||
export function toMqttConnection(options: ConnectionOptions): MqttOptions | undefined {
|
||||
@@ -71,6 +72,7 @@ export function createEmptyConnection(): ConnectionOptions {
|
||||
host: '',
|
||||
port: 1883,
|
||||
protocol: 'mqtt',
|
||||
order: Date.now(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,12 +84,14 @@ export function makeDefaultConnections() {
|
||||
id: 'mqtt.eclipseprojects.io',
|
||||
name: 'mqtt.eclipseprojects.io',
|
||||
host: 'mqtt.eclipseprojects.io',
|
||||
order: 0,
|
||||
},
|
||||
'test.mosquitto.org': {
|
||||
...createEmptyConnection(),
|
||||
id: 'test.mosquitto.org',
|
||||
name: 'test.mosquitto.org',
|
||||
host: 'test.mosquitto.org',
|
||||
order: 1,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
/**
|
||||
* React 19 compatibility shim for react-vis
|
||||
*
|
||||
* react-vis uses React internals that were removed in React 19.
|
||||
* This shim adds back the missing internals to maintain compatibility.
|
||||
*/
|
||||
import * as React from 'react'
|
||||
|
||||
// Add missing React internals that react-vis expects
|
||||
if (typeof React !== 'undefined') {
|
||||
const internals = (React as any).__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
|
||||
|
||||
if (internals) {
|
||||
// ReactCurrentOwner was removed in React 19 but react-vis expects it
|
||||
if (!internals.ReactCurrentOwner) {
|
||||
internals.ReactCurrentOwner = {
|
||||
current: null,
|
||||
}
|
||||
}
|
||||
|
||||
// ReactCurrentDispatcher compatibility
|
||||
if (!internals.ReactCurrentDispatcher) {
|
||||
internals.ReactCurrentDispatcher = {
|
||||
current: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,7 @@ export type Action =
|
||||
| ToggleCertificateSettings
|
||||
| DeleteSubscription
|
||||
| AddSubscription
|
||||
| MoveConnection
|
||||
|
||||
export enum ActionTypes {
|
||||
CONNECTION_MANAGER_SET_CONNECTIONS = 'CONNECTION_MANAGER_SET_CONNECTIONS',
|
||||
@@ -37,6 +38,7 @@ export enum ActionTypes {
|
||||
CONNECTION_MANAGER_TOGGLE_CERTIFICATE_SETTINGS = 'CONNECTION_MANAGER_TOGGLE_CERTIFICATE_SETTINGS',
|
||||
CONNECTION_MANAGER_ADD_SUBSCRIPTION = 'CONNECTION_MANAGER_ADD_SUBSCRIPTION',
|
||||
CONNECTION_MANAGER_DELETE_SUBSCRIPTION = 'CONNECTION_MANAGER_DELETE_SUBSCRIPTION',
|
||||
CONNECTION_MANAGER_MOVE_CONNECTION = 'CONNECTION_MANAGER_MOVE_CONNECTION',
|
||||
}
|
||||
|
||||
export interface SetConnections {
|
||||
@@ -85,6 +87,12 @@ export interface ToggleCertificateSettings {
|
||||
type: ActionTypes.CONNECTION_MANAGER_TOGGLE_CERTIFICATE_SETTINGS
|
||||
}
|
||||
|
||||
export interface MoveConnection {
|
||||
type: ActionTypes.CONNECTION_MANAGER_MOVE_CONNECTION
|
||||
connectionId: string
|
||||
direction: 'up' | 'down'
|
||||
}
|
||||
|
||||
export const connectionManagerReducer = createReducer(initialState, {
|
||||
CONNECTION_MANAGER_SET_CONNECTIONS: setConnections,
|
||||
CONNECTION_MANAGER_SELECT_CONNECTION: selectConnection,
|
||||
@@ -95,6 +103,7 @@ export const connectionManagerReducer = createReducer(initialState, {
|
||||
CONNECTION_MANAGER_TOGGLE_CERTIFICATE_SETTINGS: toggleCertificateSettings,
|
||||
CONNECTION_MANAGER_DELETE_SUBSCRIPTION: deleteSubscription,
|
||||
CONNECTION_MANAGER_ADD_SUBSCRIPTION: addSubscription,
|
||||
CONNECTION_MANAGER_MOVE_CONNECTION: moveConnection,
|
||||
})
|
||||
|
||||
function setConnections(state: ConnectionManagerState, action: SetConnections): ConnectionManagerState {
|
||||
@@ -222,3 +231,40 @@ function updateConnection(state: ConnectionManagerState, action: UpdateConnectio
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function moveConnection(state: ConnectionManagerState, action: MoveConnection): ConnectionManagerState {
|
||||
const connections = Object.values(state.connections).sort((a, b) => (a.order || 0) - (b.order || 0))
|
||||
const currentIndex = connections.findIndex(c => c.id === action.connectionId)
|
||||
|
||||
if (currentIndex === -1) {
|
||||
return state
|
||||
}
|
||||
|
||||
const targetIndex = action.direction === 'up' ? currentIndex - 1 : currentIndex + 1
|
||||
|
||||
// Can't move beyond bounds
|
||||
if (targetIndex < 0 || targetIndex >= connections.length) {
|
||||
return state
|
||||
}
|
||||
|
||||
// Swap order values
|
||||
const currentConnection = connections[currentIndex]
|
||||
const targetConnection = connections[targetIndex]
|
||||
const currentOrder = currentConnection.order || 0
|
||||
const targetOrder = targetConnection.order || 0
|
||||
|
||||
return {
|
||||
...state,
|
||||
connections: {
|
||||
...state.connections,
|
||||
[currentConnection.id]: {
|
||||
...currentConnection,
|
||||
order: targetOrder,
|
||||
},
|
||||
[targetConnection.id]: {
|
||||
...targetConnection,
|
||||
order: currentOrder,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
import 'mocha'
|
||||
import { expect } from 'chai'
|
||||
import { connectionManagerReducer, ConnectionManagerState, ActionTypes } from '../ConnectionManager'
|
||||
import { createEmptyConnection } from '../../model/ConnectionOptions'
|
||||
|
||||
describe('ConnectionManager - moveConnection', () => {
|
||||
it('should move connection up', () => {
|
||||
const connection1 = { ...createEmptyConnection(), id: 'conn1', name: 'Connection 1', order: 0 }
|
||||
const connection2 = { ...createEmptyConnection(), id: 'conn2', name: 'Connection 2', order: 1 }
|
||||
const connection3 = { ...createEmptyConnection(), id: 'conn3', name: 'Connection 3', order: 2 }
|
||||
|
||||
const initialState: ConnectionManagerState = {
|
||||
connections: {
|
||||
conn1: connection1,
|
||||
conn2: connection2,
|
||||
conn3: connection3,
|
||||
},
|
||||
selected: undefined,
|
||||
showAdvancedSettings: false,
|
||||
showCertificateSettings: false,
|
||||
}
|
||||
|
||||
const action = {
|
||||
type: ActionTypes.CONNECTION_MANAGER_MOVE_CONNECTION,
|
||||
connectionId: 'conn2',
|
||||
direction: 'up' as 'up',
|
||||
}
|
||||
|
||||
const newState = connectionManagerReducer(initialState, action)
|
||||
|
||||
expect(newState.connections.conn1.order).to.equal(1)
|
||||
expect(newState.connections.conn2.order).to.equal(0)
|
||||
expect(newState.connections.conn3.order).to.equal(2)
|
||||
})
|
||||
|
||||
it('should move connection down', () => {
|
||||
const connection1 = { ...createEmptyConnection(), id: 'conn1', name: 'Connection 1', order: 0 }
|
||||
const connection2 = { ...createEmptyConnection(), id: 'conn2', name: 'Connection 2', order: 1 }
|
||||
const connection3 = { ...createEmptyConnection(), id: 'conn3', name: 'Connection 3', order: 2 }
|
||||
|
||||
const initialState: ConnectionManagerState = {
|
||||
connections: {
|
||||
conn1: connection1,
|
||||
conn2: connection2,
|
||||
conn3: connection3,
|
||||
},
|
||||
selected: undefined,
|
||||
showAdvancedSettings: false,
|
||||
showCertificateSettings: false,
|
||||
}
|
||||
|
||||
const action = {
|
||||
type: ActionTypes.CONNECTION_MANAGER_MOVE_CONNECTION,
|
||||
connectionId: 'conn2',
|
||||
direction: 'down' as 'down',
|
||||
}
|
||||
|
||||
const newState = connectionManagerReducer(initialState, action)
|
||||
|
||||
expect(newState.connections.conn1.order).to.equal(0)
|
||||
expect(newState.connections.conn2.order).to.equal(2)
|
||||
expect(newState.connections.conn3.order).to.equal(1)
|
||||
})
|
||||
|
||||
it('should not move connection up when already at top', () => {
|
||||
const connection1 = { ...createEmptyConnection(), id: 'conn1', name: 'Connection 1', order: 0 }
|
||||
const connection2 = { ...createEmptyConnection(), id: 'conn2', name: 'Connection 2', order: 1 }
|
||||
|
||||
const initialState: ConnectionManagerState = {
|
||||
connections: {
|
||||
conn1: connection1,
|
||||
conn2: connection2,
|
||||
},
|
||||
selected: undefined,
|
||||
showAdvancedSettings: false,
|
||||
showCertificateSettings: false,
|
||||
}
|
||||
|
||||
const action = {
|
||||
type: ActionTypes.CONNECTION_MANAGER_MOVE_CONNECTION,
|
||||
connectionId: 'conn1',
|
||||
direction: 'up' as 'up',
|
||||
}
|
||||
|
||||
const newState = connectionManagerReducer(initialState, action)
|
||||
|
||||
expect(newState.connections.conn1.order).to.equal(0)
|
||||
expect(newState.connections.conn2.order).to.equal(1)
|
||||
})
|
||||
|
||||
it('should not move connection down when already at bottom', () => {
|
||||
const connection1 = { ...createEmptyConnection(), id: 'conn1', name: 'Connection 1', order: 0 }
|
||||
const connection2 = { ...createEmptyConnection(), id: 'conn2', name: 'Connection 2', order: 1 }
|
||||
|
||||
const initialState: ConnectionManagerState = {
|
||||
connections: {
|
||||
conn1: connection1,
|
||||
conn2: connection2,
|
||||
},
|
||||
selected: undefined,
|
||||
showAdvancedSettings: false,
|
||||
showCertificateSettings: false,
|
||||
}
|
||||
|
||||
const action = {
|
||||
type: ActionTypes.CONNECTION_MANAGER_MOVE_CONNECTION,
|
||||
connectionId: 'conn2',
|
||||
direction: 'down' as 'down',
|
||||
}
|
||||
|
||||
const newState = connectionManagerReducer(initialState, action)
|
||||
|
||||
expect(newState.connections.conn1.order).to.equal(0)
|
||||
expect(newState.connections.conn2.order).to.equal(1)
|
||||
})
|
||||
|
||||
it('should handle non-existent connection', () => {
|
||||
const connection1 = { ...createEmptyConnection(), id: 'conn1', name: 'Connection 1', order: 0 }
|
||||
|
||||
const initialState: ConnectionManagerState = {
|
||||
connections: {
|
||||
conn1: connection1,
|
||||
},
|
||||
selected: undefined,
|
||||
showAdvancedSettings: false,
|
||||
showCertificateSettings: false,
|
||||
}
|
||||
|
||||
const action = {
|
||||
type: ActionTypes.CONNECTION_MANAGER_MOVE_CONNECTION,
|
||||
connectionId: 'nonexistent',
|
||||
direction: 'up' as 'up',
|
||||
}
|
||||
|
||||
const newState = connectionManagerReducer(initialState, action)
|
||||
|
||||
expect(newState.connections.conn1.order).to.equal(0)
|
||||
})
|
||||
})
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
"sourceMap": true,
|
||||
"module": "esnext",
|
||||
"target": "ES2020",
|
||||
"jsx": "react-jsx",
|
||||
"jsx": "react",
|
||||
"paths": {
|
||||
"react": ["./node_modules/@types/react"]
|
||||
},
|
||||
|
||||
+153
-114
@@ -52,7 +52,7 @@
|
||||
dependencies:
|
||||
"@babel/types" "^7.28.5"
|
||||
|
||||
"@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.26.0", "@babel/runtime@^7.28.4", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
|
||||
"@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.23.9", "@babel/runtime@^7.26.0", "@babel/runtime@^7.28.4", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2":
|
||||
version "7.28.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.28.4.tgz#a70226016fabe25c5783b2f22d3e1c9bc5ca3326"
|
||||
integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==
|
||||
@@ -114,7 +114,7 @@
|
||||
source-map "^0.5.7"
|
||||
stylis "4.2.0"
|
||||
|
||||
"@emotion/cache@^11.14.0":
|
||||
"@emotion/cache@^11.13.5", "@emotion/cache@^11.14.0":
|
||||
version "11.14.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.14.0.tgz#ee44b26986eeb93c8be82bb92f1f7a9b21b2ed76"
|
||||
integrity sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==
|
||||
@@ -172,7 +172,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.4.0.tgz#c9299c34d248bc26e82563735f78953d2efca83c"
|
||||
integrity sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==
|
||||
|
||||
"@emotion/styled@^11.14.1":
|
||||
"@emotion/styled@^11.14.0":
|
||||
version "11.14.1"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.14.1.tgz#8c34bed2948e83e1980370305614c20955aacd1c"
|
||||
integrity sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==
|
||||
@@ -204,6 +204,33 @@
|
||||
resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz#5e13fac887f08c44f76b0ccaf3370eb00fec9bb6"
|
||||
integrity sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==
|
||||
|
||||
"@floating-ui/core@^1.7.3":
|
||||
version "1.7.3"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.7.3.tgz#462d722f001e23e46d86fd2bd0d21b7693ccb8b7"
|
||||
integrity sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==
|
||||
dependencies:
|
||||
"@floating-ui/utils" "^0.2.10"
|
||||
|
||||
"@floating-ui/dom@^1.7.4":
|
||||
version "1.7.4"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.7.4.tgz#ee667549998745c9c3e3e84683b909c31d6c9a77"
|
||||
integrity sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==
|
||||
dependencies:
|
||||
"@floating-ui/core" "^1.7.3"
|
||||
"@floating-ui/utils" "^0.2.10"
|
||||
|
||||
"@floating-ui/react-dom@^2.0.8":
|
||||
version "2.1.6"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.1.6.tgz#189f681043c1400561f62972f461b93f01bf2231"
|
||||
integrity sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==
|
||||
dependencies:
|
||||
"@floating-ui/dom" "^1.7.4"
|
||||
|
||||
"@floating-ui/utils@^0.2.10":
|
||||
version "0.2.10"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.10.tgz#a2a1e3812d14525f725d011a73eceb41fef5bc1c"
|
||||
integrity sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==
|
||||
|
||||
"@jridgewell/gen-mapping@^0.3.0":
|
||||
version "0.3.5"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36"
|
||||
@@ -315,48 +342,71 @@
|
||||
resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b"
|
||||
integrity sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==
|
||||
|
||||
"@mui/core-downloads-tracker@^7.3.6":
|
||||
version "7.3.6"
|
||||
resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-7.3.6.tgz#e7e3a4dc161a377be8224aa988410e89571ab40a"
|
||||
integrity sha512-QaYtTHlr8kDFN5mE1wbvVARRKH7Fdw1ZuOjBJcFdVpfNfRYKF3QLT4rt+WaB6CKJvpqxRsmEo0kpYinhH5GeHg==
|
||||
|
||||
"@mui/icons-material@^7.3.6":
|
||||
version "7.3.6"
|
||||
resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-7.3.6.tgz#c0092afd04a661603d9751c851e0099a27c1d556"
|
||||
integrity sha512-0FfkXEj22ysIq5pa41A2NbcAhJSvmcZQ/vcTIbjDsd6hlslG82k5BEBqqS0ZJprxwIL3B45qpJ+bPHwJPlF7uQ==
|
||||
"@mui/base@5.0.0-beta.40-1":
|
||||
version "5.0.0-beta.40-1"
|
||||
resolved "https://registry.yarnpkg.com/@mui/base/-/base-5.0.0-beta.40-1.tgz#6da6229e5e675e811f319149f6e29d7a77522851"
|
||||
integrity sha512-agKXuNNy0bHUmeU7pNmoZwNFr7Hiyhojkb9+2PVyDG5+6RafYuyMgbrav8CndsB7KUc/U51JAw9vKNDLYBzaUA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
|
||||
"@mui/lab@^7.0.1-beta.20":
|
||||
version "7.0.1-beta.20"
|
||||
resolved "https://registry.yarnpkg.com/@mui/lab/-/lab-7.0.1-beta.20.tgz#e63282cf686c46c44b36066c99dc7e3cf3acdf86"
|
||||
integrity sha512-xZW+gLO0htUjL02lZRhrziyOuz/azdwqgyiyjKvn52W2wbbcXtFhDVp3ns7YYiQAF9I+Sgu1g1a2HZutOlqeWw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
"@mui/system" "^7.3.6"
|
||||
"@mui/types" "^7.4.9"
|
||||
"@mui/utils" "^7.3.6"
|
||||
clsx "^2.1.1"
|
||||
"@babel/runtime" "^7.23.9"
|
||||
"@floating-ui/react-dom" "^2.0.8"
|
||||
"@mui/types" "~7.2.15"
|
||||
"@mui/utils" "^5.17.1"
|
||||
"@popperjs/core" "^2.11.8"
|
||||
clsx "^2.1.0"
|
||||
prop-types "^15.8.1"
|
||||
|
||||
"@mui/material@^7.3.6":
|
||||
version "7.3.6"
|
||||
resolved "https://registry.yarnpkg.com/@mui/material/-/material-7.3.6.tgz#6bd4705ca97d80fd5ae1b6b2b7c56ba0cfab0d6a"
|
||||
integrity sha512-R4DaYF3dgCQCUAkr4wW1w26GHXcf5rCmBRHVBuuvJvaGLmZdD8EjatP80Nz5JCw0KxORAzwftnHzXVnjR8HnFw==
|
||||
"@mui/core-downloads-tracker@^5.18.0":
|
||||
version "5.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@mui/core-downloads-tracker/-/core-downloads-tracker-5.18.0.tgz#85019a8704b0f63305fc5600635ee663810f2b66"
|
||||
integrity sha512-jbhwoQ1AY200PSSOrNXmrFCaSDSJWP7qk6urkTmIirvRXDROkqe+QwcLlUiw/PrREwsIF/vm3/dAXvjlMHF0RA==
|
||||
|
||||
"@mui/icons-material@^5.18.0":
|
||||
version "5.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@mui/icons-material/-/icons-material-5.18.0.tgz#97d87f1b7bee5fa7b9ba844518631de3112c1e57"
|
||||
integrity sha512-1s0vEZj5XFXDMmz3Arl/R7IncFqJ+WQ95LDp1roHWGDE2oCO3IS4/hmiOv1/8SD9r6B7tv9GLiqVZYHo+6PkTg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
"@mui/core-downloads-tracker" "^7.3.6"
|
||||
"@mui/system" "^7.3.6"
|
||||
"@mui/types" "^7.4.9"
|
||||
"@mui/utils" "^7.3.6"
|
||||
"@babel/runtime" "^7.23.9"
|
||||
|
||||
"@mui/lab@^5.0.0-alpha.177":
|
||||
version "5.0.0-alpha.177"
|
||||
resolved "https://registry.yarnpkg.com/@mui/lab/-/lab-5.0.0-alpha.177.tgz#a02f43b36d7e204166706f2c27640f378e3d7c88"
|
||||
integrity sha512-bdCxxtNjlWAgN9rtrwlmFydJ1qxA3IIbb6OlomGFsIXw0zGoHomLyjvh72q/R3yUAC0kvSef18cHY1UalLylyQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.23.9"
|
||||
"@mui/base" "5.0.0-beta.40-1"
|
||||
"@mui/system" "^5.18.0"
|
||||
"@mui/types" "~7.2.15"
|
||||
"@mui/utils" "^5.17.1"
|
||||
clsx "^2.1.0"
|
||||
prop-types "^15.8.1"
|
||||
|
||||
"@mui/material@^5.18.0":
|
||||
version "5.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@mui/material/-/material-5.18.0.tgz#71e72d52338252edc6f8d9461e04fdf0d61905cd"
|
||||
integrity sha512-bbH/HaJZpFtXGvWg3TsBWG4eyt3gah3E7nCNU8GLyRjVoWcA91Vm/T+sjHfUcwgJSw9iLtucfHBoq+qW/T30aA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.23.9"
|
||||
"@mui/core-downloads-tracker" "^5.18.0"
|
||||
"@mui/system" "^5.18.0"
|
||||
"@mui/types" "~7.2.15"
|
||||
"@mui/utils" "^5.17.1"
|
||||
"@popperjs/core" "^2.11.8"
|
||||
"@types/react-transition-group" "^4.4.12"
|
||||
clsx "^2.1.1"
|
||||
"@types/react-transition-group" "^4.4.10"
|
||||
clsx "^2.1.0"
|
||||
csstype "^3.1.3"
|
||||
prop-types "^15.8.1"
|
||||
react-is "^19.2.0"
|
||||
react-is "^19.0.0"
|
||||
react-transition-group "^4.4.5"
|
||||
|
||||
"@mui/private-theming@^5.17.1":
|
||||
version "5.17.1"
|
||||
resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-5.17.1.tgz#b4b6fbece27830754ef78186e3f1307dca42f295"
|
||||
integrity sha512-XMxU0NTYcKqdsG8LRmSoxERPXwMbp16sIXPcLVgLGII/bVNagX0xaheWAwFv8+zDK7tI3ajllkuD3GZZE++ICQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.23.9"
|
||||
"@mui/utils" "^5.17.1"
|
||||
prop-types "^15.8.1"
|
||||
|
||||
"@mui/private-theming@^6.4.8":
|
||||
version "6.4.9"
|
||||
resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-6.4.9.tgz#0c1d65a638a1740aad0eb715d79e76471abe8175"
|
||||
@@ -366,24 +416,14 @@
|
||||
"@mui/utils" "^6.4.9"
|
||||
prop-types "^15.8.1"
|
||||
|
||||
"@mui/private-theming@^7.3.6":
|
||||
version "7.3.6"
|
||||
resolved "https://registry.yarnpkg.com/@mui/private-theming/-/private-theming-7.3.6.tgz#1ca65a08e8f7f538d9a10ba974f1f4db5231a969"
|
||||
integrity sha512-Ws9wZpqM+FlnbZXaY/7yvyvWQo1+02Tbx50mVdNmzWEi51C51y56KAbaDCYyulOOBL6BJxuaqG8rNNuj7ivVyw==
|
||||
"@mui/styled-engine@^5.18.0":
|
||||
version "5.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-5.18.0.tgz#914cca1385bb33ce0cde31721f529c8bd7fa301c"
|
||||
integrity sha512-BN/vKV/O6uaQh2z5rXV+MBlVrEkwoS/TK75rFQ2mjxA7+NBo8qtTAOA4UaM0XeJfn7kh2wZ+xQw2HAx0u+TiBg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
"@mui/utils" "^7.3.6"
|
||||
prop-types "^15.8.1"
|
||||
|
||||
"@mui/styled-engine@^7.3.6":
|
||||
version "7.3.6"
|
||||
resolved "https://registry.yarnpkg.com/@mui/styled-engine/-/styled-engine-7.3.6.tgz#dde8e6ae32c9b5b400dcd37afd9514a5344f7d91"
|
||||
integrity sha512-+wiYbtvj+zyUkmDB+ysH6zRjuQIJ+CM56w0fEXV+VDNdvOuSywG+/8kpjddvvlfMLsaWdQe5oTuYGBcodmqGzQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
"@emotion/cache" "^11.14.0"
|
||||
"@babel/runtime" "^7.23.9"
|
||||
"@emotion/cache" "^11.13.5"
|
||||
"@emotion/serialize" "^1.3.3"
|
||||
"@emotion/sheet" "^1.4.0"
|
||||
csstype "^3.1.3"
|
||||
prop-types "^15.8.1"
|
||||
|
||||
@@ -410,32 +450,37 @@
|
||||
jss-plugin-vendor-prefixer "^10.10.0"
|
||||
prop-types "^15.8.1"
|
||||
|
||||
"@mui/system@^7.3.6":
|
||||
version "7.3.6"
|
||||
resolved "https://registry.yarnpkg.com/@mui/system/-/system-7.3.6.tgz#460f82fc6fe1b79b8c04dc97694f6b162ffc3d25"
|
||||
integrity sha512-8fehAazkHNP1imMrdD2m2hbA9sl7Ur6jfuNweh5o4l9YPty4iaZzRXqYvBCWQNwFaSHmMEj2KPbyXGp7Bt73Rg==
|
||||
"@mui/system@^5.18.0":
|
||||
version "5.18.0"
|
||||
resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.18.0.tgz#e55331203a40584b26c5a855a07949ac8973bfb6"
|
||||
integrity sha512-ojZGVcRWqWhu557cdO3pWHloIGJdzVtxs3rk0F9L+x55LsUjcMUVkEhiF7E4TMxZoF9MmIHGGs0ZX3FDLAf0Xw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
"@mui/private-theming" "^7.3.6"
|
||||
"@mui/styled-engine" "^7.3.6"
|
||||
"@mui/types" "^7.4.9"
|
||||
"@mui/utils" "^7.3.6"
|
||||
clsx "^2.1.1"
|
||||
"@babel/runtime" "^7.23.9"
|
||||
"@mui/private-theming" "^5.17.1"
|
||||
"@mui/styled-engine" "^5.18.0"
|
||||
"@mui/types" "~7.2.15"
|
||||
"@mui/utils" "^5.17.1"
|
||||
clsx "^2.1.0"
|
||||
csstype "^3.1.3"
|
||||
prop-types "^15.8.1"
|
||||
|
||||
"@mui/types@^7.4.9":
|
||||
version "7.4.9"
|
||||
resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.4.9.tgz#99accc87920b4c8c4ce33c5076a58f7f81b528fa"
|
||||
integrity sha512-dNO8Z9T2cujkSIaCnWwprfeKmTWh97cnjkgmpFJ2sbfXLx8SMZijCYHOtP/y5nnUb/Rm2omxbDMmtUoSaUtKaw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
|
||||
"@mui/types@~7.2.24":
|
||||
"@mui/types@~7.2.15", "@mui/types@~7.2.24":
|
||||
version "7.2.24"
|
||||
resolved "https://registry.yarnpkg.com/@mui/types/-/types-7.2.24.tgz#5eff63129d9c29d80bbf2d2e561bd0690314dec2"
|
||||
integrity sha512-3c8tRt/CbWZ+pEg7QpSwbdxOk36EfmhbKf6AGZsD1EcLDLTSZoxxJ86FVtcjxvjuhdyBiWKSTGZFaXCnidO2kw==
|
||||
|
||||
"@mui/utils@^5.17.1":
|
||||
version "5.17.1"
|
||||
resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-5.17.1.tgz#72ba4ffa79f7bdf69d67458139390f18484b6e6b"
|
||||
integrity sha512-jEZ8FTqInt2WzxDV8bhImWBqeQRD99c/id/fq83H0ER9tFl+sfZlaAoCdznGvbSQQ9ividMxqSV2c7cC1vBcQg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.23.9"
|
||||
"@mui/types" "~7.2.15"
|
||||
"@types/prop-types" "^15.7.12"
|
||||
clsx "^2.1.1"
|
||||
prop-types "^15.8.1"
|
||||
react-is "^19.0.0"
|
||||
|
||||
"@mui/utils@^6.4.8", "@mui/utils@^6.4.9":
|
||||
version "6.4.9"
|
||||
resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-6.4.9.tgz#b0df01daa254c7c32a1a30b30a5179e19ef071a7"
|
||||
@@ -448,18 +493,6 @@
|
||||
prop-types "^15.8.1"
|
||||
react-is "^19.0.0"
|
||||
|
||||
"@mui/utils@^7.3.6":
|
||||
version "7.3.6"
|
||||
resolved "https://registry.yarnpkg.com/@mui/utils/-/utils-7.3.6.tgz#508fbe864832f99b215d134eb89e1198cdc66b34"
|
||||
integrity sha512-jn+Ba02O6PiFs7nKva8R2aJJ9kJC+3kQ2R0BbKNY3KQQ36Qng98GnPRFTlbwYTdMD6hLEBKaMLUktyg/rTfd2w==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.28.4"
|
||||
"@mui/types" "^7.4.9"
|
||||
"@types/prop-types" "^15.7.15"
|
||||
clsx "^2.1.1"
|
||||
prop-types "^15.8.1"
|
||||
react-is "^19.2.0"
|
||||
|
||||
"@polka/url@^1.0.0-next.24":
|
||||
version "1.0.0-next.25"
|
||||
resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.25.tgz#f077fdc0b5d0078d30893396ff4827a13f99e817"
|
||||
@@ -874,7 +907,7 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.11.tgz#2596fb352ee96a1379c657734d4b913a613ad563"
|
||||
integrity sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==
|
||||
|
||||
"@types/prop-types@^15.7.14", "@types/prop-types@^15.7.15":
|
||||
"@types/prop-types@^15.7.12", "@types/prop-types@^15.7.14":
|
||||
version "15.7.15"
|
||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.15.tgz#e6e5a86d602beaca71ce5163fadf5f95d70931c7"
|
||||
integrity sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==
|
||||
@@ -889,10 +922,10 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb"
|
||||
integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==
|
||||
|
||||
"@types/react-dom@^19.2.3":
|
||||
version "19.2.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-19.2.3.tgz#c1e305d15a52a3e508d54dca770d202cb63abf2c"
|
||||
integrity sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==
|
||||
"@types/react-dom@^18.3.5":
|
||||
version "18.3.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.7.tgz#b89ddf2cd83b4feafcc4e2ea41afdfb95a0d194f"
|
||||
integrity sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==
|
||||
|
||||
"@types/react-redux@^7.1.34":
|
||||
version "7.1.34"
|
||||
@@ -911,7 +944,7 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-transition-group@^4.4.11", "@types/react-transition-group@^4.4.12":
|
||||
"@types/react-transition-group@^4.4.10", "@types/react-transition-group@^4.4.11":
|
||||
version "4.4.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-transition-group/-/react-transition-group-4.4.12.tgz#b5d76568485b02a307238270bfe96cb51ee2a044"
|
||||
integrity sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==
|
||||
@@ -925,11 +958,12 @@
|
||||
"@types/scheduler" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
"@types/react@^19.2.7":
|
||||
version "19.2.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-19.2.7.tgz#84e62c0f23e8e4e5ac2cadcea1ffeacccae7f62f"
|
||||
integrity sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==
|
||||
"@types/react@^18.3.18":
|
||||
version "18.3.27"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.27.tgz#74a3b590ea183983dc65a474dc17553ae1415c34"
|
||||
integrity sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
csstype "^3.2.2"
|
||||
|
||||
"@types/retry@0.12.2":
|
||||
@@ -1176,7 +1210,7 @@ accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.8:
|
||||
mime-types "~2.1.34"
|
||||
negotiator "0.6.3"
|
||||
|
||||
ace-builds@^1.36.3:
|
||||
ace-builds@^1.32.8:
|
||||
version "1.43.5"
|
||||
resolved "https://registry.yarnpkg.com/ace-builds/-/ace-builds-1.43.5.tgz#2099eee8caade0cd1ee4ce3e0d2c6fb2cd7e8619"
|
||||
integrity sha512-iH5FLBKdB7SVn9GR37UgA/tpQS8OTWIxWAuq3Ofaw+Qbc69FfPXsXd9jeW7KRG2xKpKMqBDnu0tHBrCWY5QI7A==
|
||||
@@ -1549,7 +1583,7 @@ clone-deep@^4.0.1:
|
||||
kind-of "^6.0.2"
|
||||
shallow-clone "^3.0.0"
|
||||
|
||||
clsx@^2.1.1:
|
||||
clsx@^2.1.0, clsx@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
|
||||
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
|
||||
@@ -3341,7 +3375,7 @@ log-symbols@^4.1.0:
|
||||
chalk "^4.1.0"
|
||||
is-unicode-supported "^0.1.0"
|
||||
|
||||
loose-envify@^1.4.0:
|
||||
loose-envify@^1.1.0, loose-envify@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
||||
@@ -3943,30 +3977,31 @@ raw-body@~2.5.3:
|
||||
iconv-lite "~0.4.24"
|
||||
unpipe "~1.0.0"
|
||||
|
||||
react-ace@^14.0.1:
|
||||
version "14.0.1"
|
||||
resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-14.0.1.tgz#dba5761cee92a1e522be7e7dab7584df7e1aa1c7"
|
||||
integrity sha512-z6YAZ20PNf/FqmYEic//G/UK6uw0rn21g58ASgHJHl9rfE4nITQLqthr9rHMVQK4ezwohJbp2dGrZpkq979PYQ==
|
||||
react-ace@^12.0.0:
|
||||
version "12.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react-ace/-/react-ace-12.0.0.tgz#d40afc7382092109eead7227d9426f55dcc2209d"
|
||||
integrity sha512-PstU6CSMfYIJknb4su2Fa0WgLXzq2ufQgR6fjcSWuGT1hGTHkBzuKw+SncV8PuLCdSJBJc1VehPhyeXlWByG/g==
|
||||
dependencies:
|
||||
ace-builds "^1.36.3"
|
||||
ace-builds "^1.32.8"
|
||||
diff-match-patch "^1.0.5"
|
||||
lodash.get "^4.4.2"
|
||||
lodash.isequal "^4.5.0"
|
||||
prop-types "^15.8.1"
|
||||
|
||||
react-dom@^19.2.3:
|
||||
version "19.2.3"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.2.3.tgz#f0b61d7e5c4a86773889fcc1853af3ed5f215b17"
|
||||
integrity sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==
|
||||
react-dom@^18.3.1:
|
||||
version "18.3.1"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4"
|
||||
integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==
|
||||
dependencies:
|
||||
scheduler "^0.27.0"
|
||||
loose-envify "^1.1.0"
|
||||
scheduler "^0.23.2"
|
||||
|
||||
react-is@^16.13.1, react-is@^16.7.0:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||
|
||||
react-is@^19.0.0, react-is@^19.2.0:
|
||||
react-is@^19.0.0:
|
||||
version "19.2.3"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-19.2.3.tgz#eec2feb69c7fb31f77d0b5c08c10ae1c88886b29"
|
||||
integrity sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA==
|
||||
@@ -4049,10 +4084,12 @@ react-vis@^1.12.1:
|
||||
prop-types "^15.5.8"
|
||||
react-motion "^0.5.2"
|
||||
|
||||
react@^19.2.3:
|
||||
version "19.2.3"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-19.2.3.tgz#d83e5e8e7a258cf6b4fe28640515f99b87cd19b8"
|
||||
integrity sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==
|
||||
react@^18.3.1:
|
||||
version "18.3.1"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891"
|
||||
integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
|
||||
readable-stream@^2.0.1:
|
||||
version "2.3.8"
|
||||
@@ -4223,10 +4260,12 @@ safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0:
|
||||
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
|
||||
scheduler@^0.27.0:
|
||||
version "0.27.0"
|
||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.27.0.tgz#0c4ef82d67d1e5c1e359e8fc76d3a87f045fe5bd"
|
||||
integrity sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==
|
||||
scheduler@^0.23.2:
|
||||
version "0.23.2"
|
||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3"
|
||||
integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
|
||||
schema-utils@^3.0.0:
|
||||
version "3.3.0"
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
mqtt-explorer:
|
||||
image: ghcr.io/thomasnordquist/mqtt-explorer:latest
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- MQTT_EXPLORER_USERNAME=admin
|
||||
- MQTT_EXPLORER_PASSWORD=changeme
|
||||
# Uncomment to disable authentication (use only behind a secure proxy!)
|
||||
# - MQTT_EXPLORER_SKIP_AUTH=true
|
||||
volumes:
|
||||
- mqtt-explorer-data:/app/data
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000', (r) => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 40s
|
||||
|
||||
volumes:
|
||||
mqtt-explorer-data:
|
||||
+7
-14
@@ -11,21 +11,17 @@
|
||||
"start": "electron .",
|
||||
"start:server": "npx tsc && node dist/src/server.js",
|
||||
"test": "yarn test:app && yarn test:backend",
|
||||
"test:all": "yarn test:app && yarn test:backend && yarn test:demo-video",
|
||||
"test:app": "(cd app && yarn test)",
|
||||
"test:backend": "(cd backend && yarn test)",
|
||||
"test:electron": "tsc && mocha --require source-map-support/register dist/src/spec/ui-tests.spec.js",
|
||||
"test:browser": "tsc && mocha --require source-map-support/register dist/src/spec/ui-tests.spec.js",
|
||||
"test:demo-video": "npx tsc && node dist/src/spec/demoVideo.js",
|
||||
"test:app": "cd app && yarn test",
|
||||
"test:backend": "cd backend && yarn test",
|
||||
"test:ui": "tsc && mocha --require source-map-support/register dist/src/spec/ui-tests.spec.js",
|
||||
"test:ui:vnc": "tsc && ./scripts/uiTestsWithVnc.sh",
|
||||
"test:mcp": "tsc && node dist/src/spec/testMcpIntrospection.js",
|
||||
"install": "(cd app && yarn)",
|
||||
"install": "cd app && yarn && cd ..",
|
||||
"dev": "npm-run-all --parallel dev:*",
|
||||
"dev:app": "(cd app && npm run dev)",
|
||||
"dev:app": "cd app && npm run dev",
|
||||
"dev:electron": "tsc && electron . --development",
|
||||
"dev:server": "npm-run-all --parallel dev:server:*",
|
||||
"dev:server:app": "(cd app && npx webpack-dev-server --config webpack.browser.config.mjs --mode development --progress)",
|
||||
"dev:server:app": "cd app && npx webpack-dev-server --config webpack.browser.config.mjs --mode development --progress",
|
||||
"dev:server:backend": "tsc && node dist/src/server.js",
|
||||
"lint": "npm-run-all --parallel lint:prettier lint:tslint lint:spellcheck",
|
||||
"lint:fix": "npm-run-all lint:tslint:fix lint:prettier:fix",
|
||||
@@ -34,8 +30,8 @@
|
||||
"lint:tslint": "tslint -p ./",
|
||||
"lint:tslint:fix": "tslint -p ./ --fix",
|
||||
"lint:spellcheck": "cspell -e ./build -e \"node_modules\" \"**/*.ts{x,}\"",
|
||||
"build": "tsc && (cd app && yarn run build)",
|
||||
"build:server": "npx tsc && (cd app && npx webpack --config webpack.browser.config.mjs --mode production)",
|
||||
"build": "tsc && cd app && yarn run build && cd ..",
|
||||
"build:server": "npx tsc && cd app && npx webpack --config webpack.browser.config.mjs --mode production && cd ..",
|
||||
"prepare-release": "tsx scripts/prepare-release.ts",
|
||||
"package": "tsx package.ts",
|
||||
"ui-test": "./scripts/uiTests.sh",
|
||||
@@ -46,9 +42,6 @@
|
||||
"type": "git",
|
||||
"url": "https://github.com/thomasnordquist/MQTT-Explorer.git"
|
||||
},
|
||||
"resolutions": {
|
||||
"@electron/node-gyp": "10.2.0-electron.1"
|
||||
},
|
||||
"build": {
|
||||
"appId": "mqtt-explorer",
|
||||
"productName": "MQTT Explorer",
|
||||
|
||||
@@ -12,30 +12,14 @@ export interface Credentials {
|
||||
export class AuthManager {
|
||||
private credentialsPath: string
|
||||
private credentials: Credentials | undefined
|
||||
private skipAuth: boolean
|
||||
|
||||
constructor(credentialsPath: string) {
|
||||
this.credentialsPath = credentialsPath
|
||||
this.skipAuth = process.env.MQTT_EXPLORER_SKIP_AUTH === 'true'
|
||||
}
|
||||
|
||||
public isAuthDisabled(): boolean {
|
||||
return this.skipAuth
|
||||
}
|
||||
|
||||
public async initialize(): Promise<void> {
|
||||
const isProduction = process.env.NODE_ENV === 'production'
|
||||
|
||||
// Check if authentication is disabled
|
||||
if (this.skipAuth) {
|
||||
console.log('='.repeat(60))
|
||||
console.log('WARNING: Authentication is DISABLED')
|
||||
console.log('MQTT_EXPLORER_SKIP_AUTH=true')
|
||||
console.log('This should only be used behind a secure authentication proxy!')
|
||||
console.log('='.repeat(60))
|
||||
return
|
||||
}
|
||||
|
||||
// Try to get credentials from environment variables
|
||||
const envUsername = process.env.MQTT_EXPLORER_USERNAME
|
||||
const envPassword = process.env.MQTT_EXPLORER_PASSWORD
|
||||
|
||||
@@ -157,16 +157,6 @@ async function startServer() {
|
||||
|
||||
// Authentication middleware for Socket.io
|
||||
io.use(async (socket, next) => {
|
||||
// Skip authentication if disabled
|
||||
if (authManager.isAuthDisabled()) {
|
||||
if (!isProduction) {
|
||||
console.log('Client connected without authentication (auth disabled)')
|
||||
}
|
||||
// Mark socket as auth-disabled for later identification
|
||||
;(socket as any).authDisabled = true
|
||||
return next()
|
||||
}
|
||||
|
||||
const { username, password } = socket.handshake.auth
|
||||
const clientIp = socket.handshake.address
|
||||
|
||||
@@ -215,17 +205,6 @@ async function startServer() {
|
||||
next()
|
||||
})
|
||||
|
||||
// Send auth status to clients on connection
|
||||
io.on('connection', (socket) => {
|
||||
// Inform client about auth status
|
||||
const authDisabled = (socket as any).authDisabled === true
|
||||
socket.emit('auth-status', { authDisabled })
|
||||
|
||||
if (!isProduction) {
|
||||
console.log(`Client connected, auth disabled: ${authDisabled}`)
|
||||
}
|
||||
})
|
||||
|
||||
// Initialize backend event bus with Socket.io
|
||||
const backendEvents = new SocketIOServerEventBus(io)
|
||||
const backendRpc = new Rpc(backendEvents)
|
||||
|
||||
@@ -16,16 +16,9 @@ export async function createTestMock(): Promise<mqtt.MqttClient> {
|
||||
return mqttClient
|
||||
}
|
||||
|
||||
// Use MQTT_BROKER_HOST from environment, default to localhost
|
||||
const brokerHost = process.env.MQTT_BROKER_HOST || '127.0.0.1'
|
||||
const brokerPort = process.env.MQTT_BROKER_PORT || '1883'
|
||||
const brokerUrl = `mqtt://${brokerHost}:${brokerPort}`
|
||||
|
||||
console.log(`Connecting to MQTT broker at ${brokerUrl}`)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
console.log('Connecting to MQTT broker at mqtt://127.0.0.1:1883...')
|
||||
const client = mqtt.connect(brokerUrl, {
|
||||
const client = mqtt.connect('mqtt://127.0.0.1:1883', {
|
||||
username: '',
|
||||
password: '',
|
||||
connectTimeout: 10000,
|
||||
@@ -35,7 +28,6 @@ export async function createTestMock(): Promise<mqtt.MqttClient> {
|
||||
client.once('connect', () => {
|
||||
console.log('Successfully connected to MQTT broker')
|
||||
mqttClient = client
|
||||
console.log(`Connected to MQTT broker at ${brokerUrl}`)
|
||||
resolve(client)
|
||||
})
|
||||
|
||||
|
||||
@@ -43,12 +43,16 @@ export async function showNumericPlot(browser: Page) {
|
||||
}
|
||||
|
||||
async function valuePreviewGuttersShowChartIcon(name: string, browser: Page) {
|
||||
const locator = browser
|
||||
.locator(`//*[contains(@data-test-type, "ShowChart")][contains(@data-test, "${name}")]`)
|
||||
.first()
|
||||
|
||||
await locator.waitFor({ state: 'visible', timeout: 30000 })
|
||||
return locator
|
||||
for (let retries = 0; retries < 2; retries += 1) {
|
||||
try {
|
||||
return await browser
|
||||
.locator(`//*[contains(@data-test-type, "ShowChart")][contains(@data-test, "${name}")]`)
|
||||
.first()
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return browser.locator(`//*[contains(@data-test-type, "ShowChart")][contains(@data-test, "${name}")]`).first()
|
||||
}
|
||||
|
||||
async function chartSettings(name: string, browser: Page) {
|
||||
|
||||
@@ -32,7 +32,7 @@ import type { MqttClient } from 'mqtt'
|
||||
* - Handle MQTT asynchronous operations properly
|
||||
*
|
||||
* Prerequisites:
|
||||
* - MQTT broker running (default: localhost:1883, configurable via MQTT_BROKER_HOST and MQTT_BROKER_PORT)
|
||||
* - MQTT broker running on localhost:1883
|
||||
* - Application built with `yarn build`
|
||||
*/
|
||||
// tslint:disable:only-arrow-functions ter-prefer-arrow-callback no-unused-expression
|
||||
@@ -125,10 +125,8 @@ describe('MQTT Explorer Comprehensive UI Tests', function () {
|
||||
page = await electronApp.firstWindow({ timeout: 30000 })
|
||||
await page.locator('//label[contains(text(), "Host")]/..//input').waitFor({ timeout: 10000 })
|
||||
|
||||
// Use MQTT_BROKER_HOST from environment, default to localhost
|
||||
const brokerHost = process.env.MQTT_BROKER_HOST || '127.0.0.1'
|
||||
console.log(`Connecting to MQTT broker at ${brokerHost}...`)
|
||||
await connectTo(brokerHost, page)
|
||||
console.log('Connecting to MQTT broker...')
|
||||
await connectTo('127.0.0.1', page)
|
||||
await sleep(3000) // Give time for all topics to load
|
||||
|
||||
// Start Sparkplug client after connection
|
||||
|
||||
@@ -667,10 +667,9 @@
|
||||
optionalDependencies:
|
||||
global-agent "^3.0.0"
|
||||
|
||||
"@electron/node-gyp@10.2.0-electron.1", "@electron/node-gyp@https://github.com/electron/node-gyp#06b29aafb7708acef8b3669835c8a7857ebc92d2":
|
||||
"@electron/node-gyp@https://github.com/electron/node-gyp#06b29aafb7708acef8b3669835c8a7857ebc92d2":
|
||||
version "10.2.0-electron.1"
|
||||
resolved "https://registry.yarnpkg.com/@electron/node-gyp/-/node-gyp-10.2.0-electron.1.tgz#ca5f125dcd0ffb275797c0c418c0d64005e0f815"
|
||||
integrity sha512-YdpRE6qSNYyf7gBv1LBDc8OAs8f/mZthzM1k4pFzodNq8dBGf64MWC5Bq8VVlgdafjQXLpINHvtRAUC9uinoqw==
|
||||
resolved "https://github.com/electron/node-gyp#06b29aafb7708acef8b3669835c8a7857ebc92d2"
|
||||
dependencies:
|
||||
env-paths "^2.2.0"
|
||||
exponential-backoff "^3.1.1"
|
||||
|
||||
Reference in New Issue
Block a user