13 Commits

Author SHA1 Message Date
57371feeb0 Release v1.3.1: Added PowerShell support and fixed theme issues 2026-01-31 11:06:25 +01:00
d398c34aa5 Migrate to Vite 7, improve UI (Copy/Download), and enhance API security 2026-01-31 10:16:49 +01:00
452e6e74cb update: moved input control buttons JSON Data area. 2026-01-31 09:10:55 +01:00
b7df3e731f Fix: the expression input box was getting reset while switching pages. Formatted the code text. 2026-01-31 09:05:07 +01:00
72d1be0bdc Enhance build-image script to support multi-architecture builds and improve help output 2026-01-30 00:06:11 +01:00
929d2ee5d2 Add upload scripts for PowerShell and NodeJS. 2026-01-30 00:04:02 +01:00
d0961c68fa Version 1.2.5 2026-01-26 18:38:47 +01:00
b1fd6da218 Update upload script to allow API URL and key to be set via environment variables 2026-01-26 16:41:04 +01:00
2a498124fe Fix API URL variable reference in upload script 2026-01-26 16:39:41 +01:00
37c73ddd2b Refactor upload script to use environment variables for API URL and key, and update usage instructions 2026-01-26 16:34:52 +01:00
03cc889cd0 Update copilot instructions with version release procedures 2026-01-23 14:46:19 +01:00
0d3832137f Fix version display issues, container shutdown problems, and modernize build scripts 2026-01-23 14:39:33 +01:00
81b3b84f81 Remove deprecated upload script 2026-01-23 12:57:21 +01:00
32 changed files with 3217 additions and 15506 deletions

View File

@@ -93,3 +93,17 @@ The application exposes a REST API for remotly uploading sample data. The API en
## Containerization ## Containerization
The application should be prepared for deployment using containerization. It should extend minimal Node 24 LTS container image. The application should be prepared for deployment using containerization. It should extend minimal Node 24 LTS container image.
## Updates
Always use `scripts/new-version.js` script to make a new release.
Correct procedure to make a new release:
- Review the code changes and ensure everything is working.
- Run `npm run build` to build the React application.
- Run `npm test` to execute the test suite and ensure all tests pass.
- Prepare a commit message describing the changes made.
- Use `scripts/new-version.js` to create a new version and commit the changes. Use `--force` option if repository is not clean.
- Don't push the changes without approval.
- Don't build docker image without approval.

View File

@@ -1,84 +0,0 @@
# Development Guide
## Quick Start Commands
```bash
# Install dependencies
npm install
# Start development server (with hot reload)
npm start
# Build for production
npm run build
# Run tests
npm test
# Serve production build locally
npm run serve
```
## Docker Commands
```bash
# Build Docker container
docker build -t jmespath-playground .
# Run Docker container
docker run -p 3000:3000 jmespath-playground
# Development with Docker Compose
docker-compose --profile dev up jmespath-playground-dev
# Production with Docker Compose
docker-compose up jmespath-playground
```
## Project Structure Overview
```
src/
├── App.js # Main component with JMESPath logic
├── App.css # App-specific styles
├── App.test.js # Basic tests
├── index.js # React entry point
├── index.css # Global styles and Bootstrap overrides
└── setupTests.js # Test configuration
```
## Key Features Implemented
1. **Real-time JMESPath Evaluation**: Uses the `jmespath` library to evaluate expressions as user types
2. **JSON Validation**: Parses and validates JSON input with error reporting
3. **Bootstrap UI**: Responsive layout with cards, buttons, and form controls
4. **Error Handling**: Clear error messages for both JSON and JMESPath syntax issues
5. **Sample Data**: Pre-loaded examples with "Load Sample" button
6. **JSON Formatting**: "Format JSON" button to prettify JSON input
7. **Clear Function**: "Clear All" button to reset all inputs
## Component Architecture
The main `App.js` component manages:
- State for JMESPath expression, JSON data, results, and errors
- Auto-evaluation using `useEffect` when inputs change
- Error handling for both JSON parsing and JMESPath evaluation
- UI event handlers for buttons and input changes
## Styling
- Bootstrap 5.3.2 for responsive grid and components
- Custom CSS for enhanced UX (color coding, hover effects)
- Gradient header with professional appearance
- Color-coded input areas (yellow for JMESPath, green for JSON, blue for results)
## Browser Compatibility
Built with React 18 and targets:
- Modern evergreen browsers
- Node.js 24 LTS compatibility
- Mobile-responsive design
## License
MIT License - see LICENSE file for details.

View File

@@ -14,31 +14,16 @@ COPY package*.json ./
# Install dependencies (production + dev for build) # Install dependencies (production + dev for build)
RUN npm ci RUN npm ci
# Copy source code and build scripts # Copy source code and build dependencies
COPY src/ ./src/ COPY src/ ./src/
COPY public/ ./public/ COPY public/ ./public/
COPY scripts/ ./scripts/ COPY scripts/ ./scripts/
COPY server.js ./server.js COPY server.js ./server.js
COPY vite.config.js ./vite.config.js
COPY index.html ./index.html
# Generate version.js if version info provided, otherwise run normal build # Build the application
RUN if [ -n "$VERSION" ]; then \ RUN npm run build
echo "// Auto-generated version file - do not edit manually" > src/version.js && \
echo "// Generated at: $(date -Iseconds)" >> src/version.js && \
echo "" >> src/version.js && \
echo "export const VERSION = '$VERSION';" >> src/version.js && \
echo "export const IS_RELEASE = $IS_RELEASE;" >> src/version.js && \
echo "export const BUILD_TIME = '$(date -Iseconds)';" >> src/version.js && \
echo "📝 Generated version.js with VERSION=$VERSION, IS_RELEASE=$IS_RELEASE"; \
fi
# Build the application (skip prebuild if we already generated version.js)
RUN if [ -n "$VERSION" ]; then \
echo "🚀 Building with pre-generated version.js" && \
npx react-scripts build; \
else \
echo "🚀 Building with version-check.js" && \
npm run build; \
fi
# Production stage # Production stage
FROM node:24-alpine AS production FROM node:24-alpine AS production
@@ -56,6 +41,10 @@ RUN npm ci --only=production && npm cache clean --force
COPY --from=builder /app/build ./build COPY --from=builder /app/build ./build
COPY --from=builder /app/server.js ./server.js COPY --from=builder /app/server.js ./server.js
# Copy entrypoint script
COPY entrypoint.sh ./entrypoint.sh
RUN chmod +x entrypoint.sh
# Expose port 3000 # Expose port 3000
EXPOSE 3000 EXPOSE 3000
@@ -64,4 +53,4 @@ ENV LISTEN_ADDR=0.0.0.0
ENV LISTEN_PORT=3000 ENV LISTEN_PORT=3000
# Start the integrated server # Start the integrated server
CMD ["node", "server.js"] ENTRYPOINT ["./entrypoint.sh"]

174
README.md
View File

@@ -8,62 +8,63 @@ A React-based web application for testing and validating JMESPath expressions ag
## Features ## Features
- 🎯 **Real-time Evaluation**: JMESPath expressions are evaluated instantly as you type - **Real-time Evaluation**: JMESPath expressions are evaluated instantly as you type
- 📝 **JSON Validation**: Built-in JSON syntax validation and error reporting - **File Upload**: Load JSON data directly from local files (supports JSON Lines format for .log files)
- 📁 **File Upload**: Load JSON data directly from local files (supports JSON Lines format for .log files) - **Remote API**: Upload sample data remotely via REST API with encrypted sessions
- 🎨 **Bootstrap UI**: Clean, responsive interface with Bootstrap styling - **Container Ready**: Containerized for easy deployment
- 🔄 **Sample Data**: Pre-loaded examples to get started quickly
- 📱 **Responsive Design**: Works on desktop, tablet, and mobile devices
- 🐳 **Docker Ready**: Containerized for easy deployment
-**Error Handling**: Clear error messages for both JSON and JMESPath syntax issues
## Application Layout
The application is divided into three main sections:
1. **Top Section**: Title and description of the tool
2. **Middle Section**: Input area for JMESPath expressions
3. **Bottom Sections**:
- **Left**: JSON data input area
- **Right**: Query results output area
## Quick Start ## Quick Start
### Prerequisites ### Prerequisites
- Node.js 24 LTS or higher - Node.js 24 LTS or higher
- npm or yarn package manager - npm package manager
### Local Development ### Local Development
1. **Clone the repository**: 1. **Clone the repository**:
```bash ```bash
git clone <repository-url> git clone <repository-url>
cd jmespath-playground cd jmespath-playground
``` ```
2. **Install dependencies**: 2. **Install dependencies**:
```bash ```bash
npm install npm install
``` ```
3. **Start the development server**: 3. **Start the development server**:
```bash ```bash
npm start npm start
``` ```
4. **Open your browser** and navigate to `http://localhost:3000` 4. **Open your browser** and navigate to `http://localhost:3000`
### Container Deployment (Optional) ### Development
You can optionally run the application in a Docker container: For development with hot reload on component changes:
```bash ```bash
# Build the Docker image npm run dev
docker build -t jmespath-playground . ```
# Run the container This runs both the React dev server (with hot reload) and the API server concurrently. The React app will proxy API requests to the backend server.
### Container Deployment
You can optionally run the application in a container:
```bash
# Build the container image
npm run build-image
# Run the container (Docker or Apple Container Tools)
docker run -p 3000:3000 jmespath-playground docker run -p 3000:3000 jmespath-playground
# or
container run -p 3000:3000 jmespath-playground
``` ```
## Usage ## Usage
@@ -82,93 +83,56 @@ docker run -p 3000:3000 jmespath-playground
- Format JSON for better readability - Format JSON for better readability
- Clear all inputs - Clear all inputs
### Example JMESPath Expressions ## Remote API Usage
Try these examples with the sample data: The application includes a REST API for uploading sample data remotely:
- `people[*].name` - Get all names
- `people[0]` - Get the first person
- `people[?age > 30]` - Filter people older than 30
- `people[*].skills[0]` - Get the first skill of each person
- `length(people)` - Count the number of people
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in development mode. The page will reload when you make edits.
### `npm test`
Launches the test runner in interactive watch mode.
### `npm run build`
Builds the app for production to the `build` folder. It correctly bundles React in production mode and optimizes the build for the best performance.
### `npm run serve`
Serves the production build locally on port 3000.
### Docker Scripts
### `npm run docker:build`
Builds a Docker container.
### `npm run docker:run`
Runs the Docker container.
## Project Structure
1. **Access API Key**: Click the key-lock button (🔒) to view your unique API key
2. **Upload Data**: Use curl or any HTTP client to upload JSON data:
```bash
curl -X POST \
-H "Content-Type: application/json" \
-H "X-API-Key: YOUR_API_KEY" \
--data @sample-data.json \
"http://your-domain.com/api/v1/upload"
``` ```
jmespath-playground/ 3. **Auto-reload**: The running app will detect new data and show a reload button
├── .github/
│ ├── workflows/ **API Endpoints:**
│ │ └── build-container.yml # CI/CD pipeline - `POST /api/v1/upload` - Upload sample data
│ └── copilot-instructions.md # AI agent instructions - `GET /api/v1/sample` - Retrieve current sample data
├── public/ - `GET /api/v1/state` - Get current state ID
│ ├── index.html - `GET /api/v1/health` - Simple health check (returns "OK")
│ ├── manifest.json - `GET /api/v1/status` - Detailed status information (JSON)
│ └── favicon.ico
├── src/ ## Server Configuration
│ ├── App.js # Main application component
│ ├── App.css # App-specific styles The server can be configured using environment variables:
│ ├── App.test.js # App tests
│ ├── index.js # React entry point **Network Settings:**
│ ├── index.css # Global styles - `LISTEN_ADDR` - Server bind address (default: `127.0.0.1`)
│ ├── setupTests.js # Test configuration - `LISTEN_PORT` - Server port (default: `3000`)
│ └── reportWebVitals.js
├── scripts/ **Session Management:**
│ ├── build.sh # Build script - `MAX_SESSIONS` - Maximum number of concurrent sessions (default: `100`)
│ └── dev.sh # Development script - `MAX_SAMPLE_SIZE` - Maximum size of uploaded sample data in bytes (default: `1048576` - 1MB)
├── Dockerfile # Docker container - `MAX_SESSION_TTL` - Session time-to-live in milliseconds (default: `3600000` - 1 hour)
├── Dockerfile.dev # Development container
├── docker-compose.yml # Container orchestration Example usage:
├── package.json # Dependencies and scripts
├── README.md # Comprehensive documentation ```bash
├── DEVELOPMENT.md # Developer guide MAX_SESSIONS=200 MAX_SAMPLE_SIZE=2097152 LISTEN_PORT=8080 node server.js
└── demo.sh # Demo script
``` ```
## Technology Stack ## Technology Stack
- **React 18.2.0**: Frontend framework - **React 18.2.0**: Frontend framework with modern hooks and components
- **Bootstrap 5.3.2**: CSS framework for styling - **Bootstrap 5.3.2**: CSS framework with dark/light theme support
- **JMESPath 0.16.0**: JMESPath expression evaluation - **JMESPath 0.16.0**: JMESPath expression evaluation library
- **Express.js 4.19.2**: Backend API server with session management
- **Node.js 24 LTS**: Runtime environment - **Node.js 24 LTS**: Runtime environment
- **Docker**: Optional containerization - **UUID 9.0.0**: Cryptographically secure session IDs
- **Container**: Containerization for easy deployment
## Contributing
1. Fork the repository
2. Create a feature branch: `git checkout -b feature/new-feature`
3. Make your changes and commit them: `git commit -m 'Add new feature'`
4. Push to the branch: `git push origin feature/new-feature`
5. Submit a pull request
## License ## License
@@ -177,7 +141,3 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
## About JMESPath ## About JMESPath
JMESPath is a query language for JSON. It allows you to declaratively specify how to extract elements from a JSON document. For more information about JMESPath syntax and capabilities, visit the [official JMESPath website](https://jmespath.org/). JMESPath is a query language for JSON. It allows you to declaratively specify how to extract elements from a JSON document. For more information about JMESPath syntax and capabilities, visit the [official JMESPath website](https://jmespath.org/).
## Support
If you encounter any issues or have questions, please [open an issue](../../issues) on GitHub.

72
bin/Upload-JMESPath.ps1 Executable file
View File

@@ -0,0 +1,72 @@
#!/usr/bin/env pwsh
[CmdletBinding()]
param(
[Parameter(HelpMessage='Path to JSON file; default: read from stdin')]
[string]$JsonFile = '-',
[Parameter(HelpMessage='API base URL')]
[string]$ApiUrl,
[Parameter(HelpMessage='API key for authentication')]
[string]$ApiKey,
[Parameter(HelpMessage='Show help')]
[switch]$Help
)
function Show-Usage {
$usage = @"
Usage: Upload-JMESPath.ps1 [-ApiUrl <url>] [-ApiKey <key>] [-JsonFile <file>]
Parameters:
-ApiUrl <url> The base URL of the JMESPath Playground API (default: http://localhost:3000 or $env:JMESPATH_PLAYGROUND_API_URL)
-ApiKey <key> The API key for authentication (can also be set via JMESPATH_PLAYGROUND_API_KEY)
-JsonFile <file> The JSON file to upload (default: stdin if not specified)
-Help Show this help message and exit
"@
Write-Output $usage
}
if ($Help) { Show-Usage; exit 0 }
# Apply environment defaults when parameters are not provided
if (-not $ApiUrl) {
if ($env:JMESPATH_PLAYGROUND_API_URL) { $ApiUrl = $env:JMESPATH_PLAYGROUND_API_URL } else { $ApiUrl = 'http://localhost:3000' }
}
if (-not $ApiKey) {
$ApiKey = $env:JMESPATH_PLAYGROUND_API_KEY
}
# Read JSON body from file or stdin
try {
if ($JsonFile -eq '-' -or [string]::IsNullOrEmpty($JsonFile)) {
$Body = [Console]::In.ReadToEnd()
} else {
if (-not (Test-Path -Path $JsonFile)) {
Write-Error "JSON file not found: $JsonFile"
exit 2
}
$Body = Get-Content -Raw -Path $JsonFile
}
} catch {
Write-Error "Failed to read JSON input: $($_.Exception.Message)"
exit 2
}
# Prepare headers
$Headers = @{ 'Accept' = 'application/json' }
if (-not [string]::IsNullOrEmpty($ApiKey)) { $Headers['X-API-Key'] = $ApiKey }
# POST to API
$Uri = "$ApiUrl/api/v1/upload"
try {
$response = Invoke-RestMethod -Uri $Uri -Method Post -Headers $Headers -ContentType 'application/json' -Body $Body -ErrorAction Stop
if ($null -ne $response) {
$response | ConvertTo-Json -Depth 10
} else {
Write-Output "Upload completed."
}
} catch {
Write-Error "Upload failed: $($_.Exception.Message)"
exit 3
}

View File

@@ -2,23 +2,33 @@
set -euo pipefail set -euo pipefail
API_URL="https://jmespath-playground.koszewscy.waw.pl" JMESPATH_PLAYGROUND_API_URL="${JMESPATH_PLAYGROUND_API_URL:-http://localhost:3000}" # May be set in bash profile
JMESPATH_PLAYGROUND_API_KEY="${JMESPATH_PLAYGROUND_API_KEY:-}" # Required if not localhost
JSON_FILE="-" JSON_FILE="-"
ADD_HEADERS=()
function usage() { function usage() {
echo "Usage: $0 [--api-url <url>] [--json-file <file>]" echo "Usage: $0 [--api-url <url>] [--json-file <file>]"
exit 1 echo
echo "Options:"
echo " --api-url <url> The base URL of the JMESPath Playground API (default: http://localhost:3000)"
echo " --api-key <key> The API key for authentication (required if not localhost)"
echo " --json-file <file> The JSON file to upload (default: stdin if not specified)"
echo " -h, --help Show this help message and exit"
echo
echo "Environment Variables:"
echo " JMESPATH_PLAYGROUND_API_URL Can be used to set the API URL"
echo " JMESPATH_PLAYGROUND_API_KEY Can be used to set the API key"
} }
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case $1 in case $1 in
--api-url) --api-url)
API_URL="$2" JMESPATH_PLAYGROUND_API_URL="$2"
shift 2 shift 2
;; ;;
--api-key) --api-key)
ADD_HEADERS+=("-H" "X-API-Key: $2") JMESPATH_PLAYGROUND_API_KEY="$2"
shift 2 shift 2
;; ;;
--json-file) --json-file)
@@ -37,11 +47,12 @@ while [[ $# -gt 0 ]]; do
esac esac
done done
ADD_HEADERS+=("-H" "X-API-Key: $JMESPATH_PLAYGROUND_API_KEY")
# Send the POST request # Send the POST request
curl -s -X POST \ curl -s -X POST \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-H "Accept: application/json" \ -H "Accept: application/json" \
"${ADD_HEADERS[@]}" \ "${ADD_HEADERS[@]}" \
--data @${JSON_FILE} \ --data @${JSON_FILE} \
"$API_URL/api/v1/upload" "$JMESPATH_PLAYGROUND_API_URL/api/v1/upload"

94
bin/upload-jmespath.mjs Executable file
View File

@@ -0,0 +1,94 @@
#!/usr/bin/env node
import fs from "fs";
import path from "path";
// Parse command-line arguments
import { parseArgs } from "util";
const args = parseArgs({
options: {
"api-url": {
type: "string",
short: "a",
default: process.env.JMESPATH_PLAYGROUND_API_URL || "http://localhost:3000",
},
"api-key": {
type: "string",
short: "k",
default: process.env.JMESPATH_PLAYGROUND_API_KEY || "",
},
"json-file": {
type: "string",
short: "j",
default: "",
},
help: { type: "boolean", short: "h" },
},
});
// Show help message
if (args.values.help) {
console.log(`
Usage: upload-jmespath.mjs [options]
Options:
-a, --api-url API base URL (default: http://localhost:3000)
-k, --api-key API key for authentication
-j, --json-file Path to the JSON file to upload
-h, --help Show this help message
`);
process.exit(0);
}
// Read the JSON from the specfied file or from stdin if no file is provided
async function readJson(filePath) {
if (filePath) {
const absolutePath = path.resolve(filePath);
const fileContent = fs.readFileSync(absolutePath, "utf-8");
return JSON.parse(fileContent);
} else {
return new Promise((resolve, reject) => {
let data = "";
process.stdin.on("data", (chunk) => {
data += chunk;
});
process.stdin.on("end", () => {
try {
resolve(JSON.parse(data));
} catch (error) {
reject(error);
}
});
});
}
}
// Upload the JSON data to the API using built-in fetch
async function uploadJson(apiUrl, apiKey, jsonData) {
const response = await fetch(`${apiUrl}/api/v1/upload`, {
method: "POST",
headers: {
"Content-Type": "application/json",
...(apiKey ? { "X-API-Key": `${apiKey}` } : {}),
},
body: JSON.stringify(jsonData),
});
if (!response.ok) {
throw new Error(`Failed to upload JSON: ${response.status} ${response.statusText}`);
}
}
// Main function
async function main() {
try {
const jsonData = await readJson(args.values["json-file"]);
await uploadJson(args.values["api-url"], args.values["api-key"], jsonData);
console.log("JSON uploaded successfully.");
} catch (error) {
console.error("Error:", error.message);
process.exit(1);
}
}
main();

79
demo.sh
View File

@@ -1,79 +0,0 @@
#!/bin/bash
# JMESPath Testing Tool - Demo Script
echo "🚀 JMESPath Testing Tool Demo"
echo "==============================="
echo ""
# Check if Node.js is installed
if command -v node &> /dev/null; then
echo "✅ Node.js version: $(node --version)"
else
echo "❌ Node.js not found. Please install Node.js 24 LTS or higher."
exit 1
fi
# Check if npm is installed
if command -v npm &> /dev/null; then
echo "✅ npm version: $(npm --version)"
else
echo "❌ npm not found. Please install npm."
exit 1
fi
# Check Docker
if command -v docker &> /dev/null; then
echo "✅ Docker available: $(docker --version | cut -d' ' -f3 | cut -d',' -f1)"
DOCKER_AVAILABLE=true
else
echo "⚠️ Docker not found"
DOCKER_AVAILABLE=false
fi
echo ""
echo "📦 Installing dependencies..."
npm install
echo ""
echo "🧪 Running tests..."
npm test -- --watchAll=false
echo ""
echo "🔨 Building React application..."
echo " (Version will be automatically tagged as -dev since not building from git tag)"
npm run build
echo ""
echo "🎉 Demo completed successfully!"
echo ""
echo "Available commands:"
echo "==================="
echo ""
echo "Development:"
echo " npm start - Start React development server (port 3000)"
echo " npm run server - Start Express API server only (port 3000)"
echo " npm test - Run test suite"
echo ""
echo "Production:"
echo " npm run build - Build React app for production"
echo " node server/server.js - Start integrated server with built app"
echo ""
if [ "$DOCKER_AVAILABLE" = true ]; then
echo "Docker:"
echo " docker build -t jmespath-playground ."
echo " docker run -p 3000:3000 jmespath-playground"
echo ""
echo "Docker Compose:"
echo " docker compose up --build"
echo " docker compose down"
else
echo "Docker (install Docker first):"
echo " docker build -t jmespath-playground ."
echo " docker run -p 3000:3000 jmespath-playground"
echo " docker compose up --build"
fi
echo ""
echo "🌐 The application will be available at:"
echo " http://localhost:3000"

3
entrypoint.sh Normal file
View File

@@ -0,0 +1,3 @@
#!/bin/sh
set -e
exec node server.js

View File

@@ -2,12 +2,11 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta name="description" content="JMESPath Testing Tool - Validate and test JMESPath expressions against JSON data" /> <meta name="description" content="JMESPath Testing Tool - Validate and test JMESPath expressions against JSON data" />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="manifest" href="/manifest.json" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700&family=Noto+Sans+Mono:wght@300;400;500;600&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Noto+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700&family=Noto+Sans+Mono:wght@300;400;500;600&display=swap" rel="stylesheet">
@@ -16,5 +15,6 @@
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/index.jsx"></script>
</body> </body>
</html> </html>

16276
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,17 @@
{ {
"name": "jmespath-playground", "name": "jmespath-playground",
"version": "1.2.2", "version": "1.3.1",
"description": "A React-based web application for testing JMESPath expressions against JSON data", "description": "A React-based web application for testing JMESPath expressions against JSON data",
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"start": "react-scripts start", "start": "vite",
"prebuild": "node scripts/version-check.js", "prebuild": "node scripts/version-check.js",
"build": "react-scripts build", "build": "vite build",
"test": "react-scripts test --watchAll=false", "preview": "vite preview",
"test:watch": "react-scripts test", "test": "vitest",
"server": "node server.js" "server": "node server.js --dev",
"dev": "concurrently \"npm start\" \"npm run server\"",
"build-image": "node scripts/build-image.js"
}, },
"engines": { "engines": {
"node": ">=24.0.0" "node": ">=24.0.0"
@@ -23,21 +25,8 @@
"jmespath": "^0.16.0", "jmespath": "^0.16.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-scripts": "^5.0.1",
"uuid": "^9.0.0" "uuid": "^9.0.0"
}, },
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"jest": {
"collectCoverageFrom": [
"src/**/*.{js,jsx,ts,tsx}",
"!src/index.js"
]
},
"browserslist": { "browserslist": {
"production": [ "production": [
">0.2%", ">0.2%",
@@ -60,6 +49,12 @@
"author": "", "author": "",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"supertest": "^7.2.2" "@vitejs/plugin-react": "^5.1.2",
"@vitest/ui": "^4.0.18",
"concurrently": "^8.2.2",
"jsdom": "^27.4.0",
"supertest": "^7.2.2",
"vite": "^7.3.1",
"vitest": "^4.0.18"
} }
} }

164
scripts/build-image.js Executable file
View File

@@ -0,0 +1,164 @@
#!/usr/bin/env node
const { execSync } = require('child_process');
const fs = require('fs');
const { parseArgs } = require('util');
function execCommand(command, description) {
try {
console.log(`${description}...`);
execSync(command, { stdio: 'inherit' });
} catch (error) {
console.error(`Error: ${description} failed`);
process.exit(1);
}
}
function getContainerTool() {
// Check for Docker first (primary tool)
try {
execSync('docker --version', { stdio: 'ignore' });
return 'docker';
} catch (error) {
// Fall back to Apple's container command
try {
execSync('container --version', { stdio: 'ignore' });
return 'container';
} catch (error) {
console.error('Error: No container tool found. Please install Docker or Apple Container Tools to build container images.');
process.exit(1);
}
}
}
function getVersion() {
try {
// Try to get version from git tag
const gitTag = execSync('git tag --points-at HEAD', { encoding: 'utf8' }).trim();
if (gitTag) {
return { version: gitTag.replace(/^v/, ''), isRelease: true };
}
} catch (error) {
// Git command failed, ignore
}
// Development build - use package.json version with -dev suffix
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
return { version: `${packageJson.version}-dev`, isRelease: false };
}
function getHostArchitecture() {
// Map Node.js architecture names to container architecture names
const archMap = {
'arm64': 'arm64',
'arm': 'arm64',
'x64': 'amd64'
};
return archMap[process.arch] || 'arm64';
}
function showHelp() {
const hostArch = getHostArchitecture();
console.log(`Build multi-architecture container images for JMESPath Playground
Usage:
build-image.js [OPTIONS]
Options:
--all-arch Build for both arm64 and amd64 (default: build for host architecture only)
--arch <arch> Target architecture (arm64 or amd64). Can be specified multiple times.
--help, -h Show this help message and exit
Examples:
build-image.js # Builds for ${hostArch} only (host architecture)
build-image.js --all-arch # Builds for both arm64 and amd64
build-image.js --arch arm64 # Builds for arm64 only
build-image.js --arch arm64 --arch amd64 # Explicitly specify both
build-image.js -h # Show help`);
}
function main() {
const { values } = parseArgs({
options: {
help: {
type: 'boolean',
short: 'h',
description: 'Show help'
},
'all-arch': {
type: 'boolean',
description: 'Build for both arm64 and amd64'
},
arch: {
type: 'string',
multiple: true,
description: 'Target architecture (arm64 or amd64)'
}
},
strict: true,
allowPositionals: false
});
if (values.help) {
showHelp();
process.exit(0);
}
const containerTool = getContainerTool();
const { version, isRelease } = getVersion();
let architectures;
if (values['all-arch']) {
architectures = ['arm64', 'amd64'];
} else if (values.arch && values.arch.length > 0) {
architectures = values.arch;
} else {
architectures = [getHostArchitecture()];
}
console.log(`Building ${isRelease ? 'release' : 'development'} version: ${version}`);
console.log(`Target architectures: ${architectures.join(', ')}`);
// Build container image
const tags = isRelease
? [
`-t skoszewski/jmespath-playground:${version}`,
`-t skoszewski/jmespath-playground:latest`
].join(' ')
: [
`-t skoszewski/jmespath-playground:dev`,
`-t skoszewski/jmespath-playground:latest`
].join(' ');
const archFlags = architectures.map(arch => `--arch ${arch}`).join(' ');
const buildCommand = `${containerTool} build ${archFlags} --build-arg VERSION="${version}" --build-arg IS_RELEASE="${isRelease}" ${tags} .`;
execCommand(buildCommand, 'Building container image');
console.log('Container image build completed successfully!');
// Show usage instructions
console.log(`\nUsage examples:`);
console.log(` build-image.js # Builds for host architecture only`);
console.log(` build-image.js --all-arch # Builds for both arm64 and amd64`);
console.log(` build-image.js --arch arm64 # Builds for arm64 only`);
console.log(` build-image.js --arch arm64 --arch amd64 # Explicitly specify both`);
if (isRelease) {
console.log(`\nTo run the container:`);
console.log(` ${containerTool} run --arch arm64 --name jmespathpg -p 3000:3000 skoszewski/jmespath-playground:${version}`);
if (containerTool === 'docker') {
console.log(`\nTo push to Docker Hub:`);
console.log(` docker push skoszewski/jmespath-playground:${version}`);
console.log(` docker push skoszewski/jmespath-playground:latest`);
}
} else {
console.log(`\nTo run the container:`);
console.log(` ${containerTool} run --arch arm64 --name jmespathpg -p 3000:3000 skoszewski/jmespath-playground:dev`);
}
}
if (require.main === module) {
main();
}

View File

@@ -1,63 +0,0 @@
#!/bin/bash
# JMESPath Testing Tool - Docker Image Build Script
set -e
echo "🐳 JMESPath Testing Tool - Docker Image Build"
echo "=============================================="
echo ""
# Check if Docker is available
if ! command -v docker &> /dev/null; then
echo "❌ Docker not found. Please install Docker to build container images."
exit 1
fi
# Determine version information for Docker build
VERSION=$(git tag --points-at HEAD 2>/dev/null | sed 's/^v//' | head -n 1)
if [ -n "$VERSION" ]; then
# We're at a tagged commit - release build
echo "📦 Building release version: $VERSION"
docker build \
--build-arg VERSION="$VERSION" \
--build-arg IS_RELEASE="true" \
-t skoszewski/jmespath-playground:$VERSION \
-t skoszewski/jmespath-playground:latest .
echo "✅ Built Docker images: skoszewski/jmespath-playground:$VERSION, skoszewski/jmespath-playground:latest"
echo ""
echo "To run the release container:"
echo " docker run -p 3000:3000 skoszewski/jmespath-playground:$VERSION"
echo " docker run -p 3000:3000 skoszewski/jmespath-playground:latest"
echo ""
echo "To push to Docker Hub:"
echo " docker push skoszewski/jmespath-playground:$VERSION"
echo " docker push skoszewski/jmespath-playground:latest"
else
# Development build
PACKAGE_VERSION=$(grep '"version"' package.json | cut -d'"' -f4)
DEV_VERSION="${PACKAGE_VERSION}-dev"
echo "📦 Building development version: $DEV_VERSION"
docker build \
--build-arg VERSION="$DEV_VERSION" \
--build-arg IS_RELEASE="false" \
-t skoszewski/jmespath-playground:dev \
-t skoszewski/jmespath-playground:latest .
echo "✅ Built Docker images: skoszewski/jmespath-playground:dev, skoszewski/jmespath-playground:latest"
echo ""
echo "To run the development container:"
echo " docker run -p 3000:3000 skoszewski/jmespath-playground:dev"
echo " docker run -p 3000:3000 skoszewski/jmespath-playground:latest"
echo ""
echo "To push to Docker Hub:"
echo " docker push skoszewski/jmespath-playground:dev"
echo " docker push skoszewski/jmespath-playground:latest"
fi
echo ""
echo "🎉 Docker image build completed successfully!"

View File

@@ -1,39 +0,0 @@
#!/bin/bash
# JMESPath Testing Tool - Build Script
set -e
echo "🚀 JMESPath Testing Tool - Build Script"
echo "======================================="
echo ""
# Check Node.js version
if command -v node &> /dev/null; then
NODE_VERSION=$(node --version | sed 's/v//')
MAJOR_VERSION=$(echo $NODE_VERSION | cut -d. -f1)
if [ "$MAJOR_VERSION" -ge 24 ]; then
echo "✅ Node.js $NODE_VERSION (compatible with v24+ requirement)"
else
echo "❌ Node.js $NODE_VERSION found, but v24+ is required"
exit 1
fi
else
echo "❌ Node.js not found. Please install Node.js 24 LTS or higher."
exit 1
fi
# Build the React application
echo "📦 Installing dependencies..."
npm install
echo "🔨 Building production bundle..."
npm run build
echo "✅ Build completed successfully!"
echo ""
echo "To run the application:"
echo " npm run server # Run integrated server locally"
echo ""
echo "To build Docker image:"
echo " scripts/build-image.sh # Build Docker container image"

View File

@@ -1,30 +0,0 @@
#!/bin/bash
# JMESPath Testing Tool - Development Script
set -e
echo "🚀 JMESPath Testing Tool - Development"
echo "====================================="
echo ""
# Check Node.js
if command -v node &> /dev/null; then
echo "✅ Node.js version: $(node --version)"
else
echo "❌ Node.js not found. Please install Node.js 24 LTS."
exit 1
fi
# Install dependencies if needed
if [ ! -d "node_modules" ]; then
echo "📦 Installing dependencies..."
npm install
fi
# Start development server
echo "🚀 Starting development server..."
echo " The app will open at http://localhost:3000"
echo " Press Ctrl+C to stop the server"
echo ""
npm start

View File

@@ -2,63 +2,57 @@
/** /**
* JMESPath Playground Upload Script (JavaScript) * JMESPath Playground Upload Script (JavaScript)
* Usage: node upload.js [-u URL] "json_file.json" * Usage: node upload.js [-u URL] [-k API_KEY] "json_file.json"
*/ */
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
const https = require('https');
const http = require('http');
const { URL } = require('url');
const { parseArgs } = require('util');
function showUsage() { function showUsage() {
const scriptName = path.basename(process.argv[1]); const scriptName = path.basename(process.argv[1]);
console.log(`Usage: node ${scriptName} [-u|--url URL] <json_file>`); console.log(`Usage: node ${scriptName} [-u|--url URL] [-k|--key API_KEY] <json_file>`);
console.log(''); console.log('');
console.log('Options:'); console.log('Options:');
console.log(' -u, --url URL API URL (default: http://localhost:3000)'); console.log(' -u, --url URL API URL (default: http://localhost:3000)');
console.log(' -k, --key API_KEY API key (not required for localhost)');
console.log(' -h, --help Show this help message'); console.log(' -h, --help Show this help message');
console.log(''); console.log('');
console.log('Example:'); console.log('Examples:');
console.log(` node ${scriptName} data.json`); console.log(` node ${scriptName} data.json`);
console.log(` node ${scriptName} -u http://example.com:3000 data.json`); console.log(` node ${scriptName} -u http://example.com:3000 -k your-api-key data.json`);
} }
function parseArguments() { function getArguments() {
const args = process.argv.slice(2); const { values, positionals } = parseArgs({
let apiUrl = 'http://localhost:3000'; args: process.argv.slice(2),
let jsonFile = ''; options: {
url: { type: 'string', short: 'u', default: 'http://localhost:3000' },
key: { type: 'string', short: 'k' },
help: { type: 'boolean', short: 'h' }
},
allowPositionals: true
});
for (let i = 0; i < args.length; i++) { if (values.help) {
const arg = args[i];
if (arg === '-u' || arg === '--url') {
if (i + 1 >= args.length) {
console.error('Error: URL argument required for -u/--url option');
process.exit(1);
}
apiUrl = args[i + 1];
i++; // Skip next argument
} else if (arg === '-h' || arg === '--help') {
showUsage(); showUsage();
process.exit(0); process.exit(0);
} else if (arg.startsWith('-')) {
console.error(`Error: Unknown option ${arg}`);
showUsage();
process.exit(1);
} else {
if (jsonFile) {
console.error('Error: Multiple JSON files specified');
process.exit(1);
}
jsonFile = arg;
}
} }
if (!jsonFile) { if (positionals.length !== 1) {
console.error('Error: JSON file required'); console.error('Error: JSON file required');
showUsage(); showUsage();
process.exit(1); process.exit(1);
} }
return { apiUrl, jsonFile }; return {
apiUrl: values.url,
apiKey: values.key || '',
jsonFile: positionals[0]
};
} }
async function validateJsonFile(jsonFile) { async function validateJsonFile(jsonFile) {
@@ -80,28 +74,84 @@ async function validateJsonFile(jsonFile) {
} }
} }
async function uploadData(apiUrl, jsonFile, jsonData) { function isLocalhost(url) {
console.log('Uploading sample data to JMESPath Playground...');
console.log(`JSON file: ${jsonFile}`);
console.log(`API URL: ${apiUrl}`);
console.log('');
try { try {
const response = await fetch(`${apiUrl}/api/v1/upload`, { const parsed = new URL(url);
method: 'POST', const hostname = parsed.hostname;
headers: { return hostname === 'localhost' ||
hostname === '127.0.0.1' ||
hostname.startsWith('127.') ||
hostname === '::1';
} catch {
return false;
}
}
function makeRequest(url, options) {
return new Promise((resolve, reject) => {
const parsedUrl = new URL(url);
const isHttps = parsedUrl.protocol === 'https:';
const client = isHttps ? https : http;
const requestOptions = {
hostname: parsedUrl.hostname,
port: parsedUrl.port,
path: parsedUrl.pathname,
method: options.method || 'GET',
headers: options.headers || {}
};
const req = client.request(requestOptions, (res) => {
let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
resolve({
ok: res.statusCode >= 200 && res.statusCode < 300,
status: res.statusCode,
json: () => Promise.resolve(JSON.parse(data))
});
});
});
req.on('error', reject);
if (options.body) {
req.write(options.body);
}
req.end();
});
}
async function uploadData(apiUrl, apiKey, jsonFile, jsonData) {
try {
const headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, };
// Only send API key for non-localhost requests
const isLocal = isLocalhost(apiUrl);
if (!isLocal && apiKey) {
headers['X-API-Key'] = apiKey;
} else if (!isLocal && !apiKey) {
console.error('Error: API key required for non-localhost URLs');
console.error('Use -k/--key option to specify API key');
process.exit(1);
}
const response = await makeRequest(`${apiUrl}/api/v1/upload`, {
method: 'POST',
headers: headers,
body: jsonData body: jsonData
}); });
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); const errorData = await response.json().catch(() => ({}));
throw new Error(`HTTP ${response.status}: ${errorData.error || 'Upload failed'}`);
} }
console.log('Sample data uploaded successfully!'); const result = await response.json();
console.log(`Open ${apiUrl} in your browser to see the reload button.`); console.log(JSON.stringify(result));
console.log('You can then enter your JMESPath expression in the web interface.');
} catch (error) { } catch (error) {
console.error('Error uploading data:', error.message); console.error('Error uploading data:', error.message);
@@ -110,9 +160,9 @@ async function uploadData(apiUrl, jsonFile, jsonData) {
} }
async function main() { async function main() {
const { apiUrl, jsonFile } = parseArguments(); const { apiUrl, apiKey, jsonFile } = getArguments();
const jsonData = await validateJsonFile(jsonFile); const jsonData = await validateJsonFile(jsonFile);
await uploadData(apiUrl, jsonFile, jsonData); await uploadData(apiUrl, apiKey, jsonFile, jsonData);
} }
// Run the script // Run the script

View File

@@ -1,82 +0,0 @@
#!/bin/bash
# JMESPath Playground Upload Script
# Usage: ./upload.sh [-u URL] "json_file.json"
show_usage() {
echo "Usage: $0 [-u|--url URL] <json_file>"
echo ""
echo "Options:"
echo " -u, --url URL API URL (default: http://localhost:3000)"
echo " -h, --help Show this help message"
echo ""
echo "Example:"
echo " $0 data.json"
echo " $0 -u http://example.com:3000 data.json"
}
# Parse command line options
API_URL="http://localhost:3000"
JSON_FILE=""
while [[ $# -gt 0 ]]; do
case $1 in
-u|--url)
API_URL="$2"
shift 2
;;
-h|--help)
show_usage
exit 0
;;
-*)
echo "Error: Unknown option $1"
show_usage
exit 1
;;
*)
if [ -z "$JSON_FILE" ]; then
JSON_FILE="$1"
else
echo "Error: Multiple JSON files specified"
exit 1
fi
shift
;;
esac
done
if [ -z "$JSON_FILE" ]; then
echo "Error: JSON file required"
show_usage
exit 1
fi
if [ ! -f "$JSON_FILE" ]; then
echo "Error: JSON file '$JSON_FILE' not found"
exit 1
fi
# Validate JSON with jq if available
if command -v jq >/dev/null 2>&1; then
if ! jq . "$JSON_FILE" >/dev/null 2>&1; then
echo "Error: '$JSON_FILE' contains invalid JSON"
exit 1
fi
fi
echo "Uploading sample data to JMESPath Playground..."
echo "JSON file: $JSON_FILE"
echo "API URL: $API_URL"
echo
# Upload the JSON data
curl -s -X POST \
-H "Content-Type: application/json" \
--data @"$JSON_FILE" \
"$API_URL/api/v1/upload"
echo
echo "Sample data uploaded successfully!"
echo "Open $API_URL in your browser to see the reload button."
echo "You can then enter your JMESPath expression in the web interface."

View File

@@ -22,15 +22,15 @@ try {
console.log(`✅ Building release version ${version} (tagged: ${gitTag})`); console.log(`✅ Building release version ${version} (tagged: ${gitTag})`);
isRelease = true; isRelease = true;
} else { } else {
// We're not at a tagged commit - add -dev suffix // We're not at a tagged commit - use unknown version
version = `${version}-dev`; version = 'unknown';
console.log(`📦 Building development version ${version}`); console.log(`📦 Building development version with unknown version`);
isRelease = false; isRelease = false;
} }
} catch (error) { } catch (error) {
// Git command failed (maybe not in a git repo) // Git command failed (maybe not in a git repo)
version = `${version}-dev`; version = 'unknown';
console.log(`⚠️ Cannot determine git status, using development version ${version}`); console.log(`⚠️ Cannot determine git status, using unknown version`);
isRelease = false; isRelease = false;
} }

402
server.js
View File

@@ -1,7 +1,9 @@
const express = require('express'); const express = require("express");
const path = require('path'); const path = require("path");
const crypto = require('crypto'); const crypto = require("crypto");
const { v4: uuidv4 } = require('uuid'); const os = require("os");
const { v4: uuidv4 } = require("uuid");
const { parseArgs } = require("util");
// Environment configuration // Environment configuration
const MAX_SESSIONS = parseInt(process.env.MAX_SESSIONS) || 100; const MAX_SESSIONS = parseInt(process.env.MAX_SESSIONS) || 100;
@@ -9,54 +11,29 @@ const MAX_SAMPLE_SIZE = parseInt(process.env.MAX_SAMPLE_SIZE) || 1024 * 1024; //
const MAX_SESSION_TTL = parseInt(process.env.MAX_SESSION_TTL) || 60 * 60 * 1000; // 1 hour const MAX_SESSION_TTL = parseInt(process.env.MAX_SESSION_TTL) || 60 * 60 * 1000; // 1 hour
// Utility functions for encryption // Utility functions for encryption
function generateSalt() {
return crypto.randomBytes(16);
}
function isLocalhostRequest(req) {
// Get client IP with fallback options
const forwarded = req.get('X-Forwarded-For');
const ip = forwarded ? forwarded.split(',')[0].trim() :
req.ip ||
req.connection.remoteAddress ||
req.socket.remoteAddress ||
'127.0.0.1';
const host = req.get('host') || '';
// Check for localhost IP addresses (IPv4 and IPv6)
const localhostIPs = ['127.0.0.1', '::1', '::ffff:127.0.0.1', 'localhost'];
const isLocalIP = localhostIPs.includes(ip) || ip.startsWith('127.') || ip === '::1';
// Check for localhost hostnames
const isLocalHost = host.startsWith('localhost:') || host.startsWith('127.0.0.1:') || host === 'localhost' || host === '127.0.0.1';
return isLocalIP || isLocalHost;
}
function encrypt(data, key) { function encrypt(data, key) {
try { try {
const algorithm = 'aes-256-gcm'; const algorithm = "aes-256-gcm";
const iv = crypto.randomBytes(16); const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, key, iv); const cipher = crypto.createCipheriv(algorithm, key, iv);
cipher.setAAD(Buffer.from('session-data')); cipher.setAAD(Buffer.from("session-data"));
let encrypted = cipher.update(JSON.stringify(data), 'utf8'); let encrypted = cipher.update(JSON.stringify(data), "utf8");
encrypted = Buffer.concat([encrypted, cipher.final()]); encrypted = Buffer.concat([encrypted, cipher.final()]);
const authTag = cipher.getAuthTag(); const authTag = cipher.getAuthTag();
return { return {
iv: iv.toString('hex'), iv: iv.toString("hex"),
data: encrypted.toString('hex'), data: encrypted.toString("hex"),
tag: authTag.toString('hex') tag: authTag.toString("hex"),
}; };
} catch (error) { } catch (error) {
console.error('⚠️ Encryption exception:', { console.error("⚠️ Encryption exception:", {
message: error.message, message: error.message,
algorithm: 'aes-256-gcm', algorithm: "aes-256-gcm",
keyLength: key ? key.length : 'undefined', keyLength: key ? key.length : "undefined",
timestamp: new Date().toISOString() timestamp: new Date().toISOString(),
}); });
throw new Error(`Encryption failed: ${error.message}`); throw new Error(`Encryption failed: ${error.message}`);
} }
@@ -64,39 +41,40 @@ function encrypt(data, key) {
function decrypt(encryptedObj, key) { function decrypt(encryptedObj, key) {
try { try {
const algorithm = 'aes-256-gcm'; const algorithm = "aes-256-gcm";
const iv = Buffer.from(encryptedObj.iv, 'hex'); const iv = Buffer.from(encryptedObj.iv, "hex");
const decipher = crypto.createDecipheriv(algorithm, key, iv); const decipher = crypto.createDecipheriv(algorithm, key, iv);
decipher.setAAD(Buffer.from('session-data')); decipher.setAAD(Buffer.from("session-data"));
decipher.setAuthTag(Buffer.from(encryptedObj.tag, 'hex')); decipher.setAuthTag(Buffer.from(encryptedObj.tag, "hex"));
let decrypted = decipher.update(Buffer.from(encryptedObj.data, 'hex'), null, 'utf8'); let decrypted = decipher.update(
decrypted += decipher.final('utf8'); Buffer.from(encryptedObj.data, "hex"),
null,
"utf8",
);
decrypted += decipher.final("utf8");
return JSON.parse(decrypted); return JSON.parse(decrypted);
} catch (error) { } catch (error) {
console.error('⚠️ Decryption exception:', { console.error("⚠️ Decryption exception:", {
message: error.message, message: error.message,
algorithm: 'aes-256-gcm', algorithm: "aes-256-gcm",
keyLength: key ? key.length : 'undefined', keyLength: key ? key.length : "undefined",
hasIV: !!encryptedObj.iv, hasIV: !!encryptedObj.iv,
hasTag: !!encryptedObj.tag, hasTag: !!encryptedObj.tag,
hasData: !!encryptedObj.data, hasData: !!encryptedObj.data,
timestamp: new Date().toISOString() timestamp: new Date().toISOString(),
}); });
throw new Error(`Decryption failed: ${error.message}`); throw new Error(`Decryption failed: ${error.message}`);
} }
} }
// For localhost requests, use a consistent API key so sessions persist
const LOCALHOST_API_KEY = 'localhost0123456789abcdef0123456789';
function isValidApiKey(apiKey) { function isValidApiKey(apiKey) {
return typeof apiKey === 'string' && /^[0-9a-f]{32}$/i.test(apiKey); return typeof apiKey === "string" && /^[0-9a-f]{32}$/i.test(apiKey);
} }
function getSessionId(apiKey) { function getSessionId(apiKey) {
return crypto.createHash('sha256').update(apiKey).digest('hex'); return crypto.createHash("sha256").update(apiKey).digest("hex");
} }
function generateSalt() { function generateSalt() {
@@ -104,19 +82,38 @@ function generateSalt() {
} }
function deriveKey(apiKey, salt) { function deriveKey(apiKey, salt) {
return crypto.pbkdf2Sync(apiKey, salt, 10000, 32, 'sha256'); return crypto.pbkdf2Sync(apiKey, salt, 100000, 32, "sha256");
} }
// Create Express app // Create Express app
function createApp() { function createApp(devMode = false) {
const app = express(); const app = express();
// Trust proxy to get real client IP (needed for localhost detection) // Trust proxy to get real client IP (needed for localhost detection)
app.set('trust proxy', true); app.set("trust proxy", true);
// Middleware // Middleware
app.use(express.json({ limit: MAX_SAMPLE_SIZE })); app.use(express.json({ limit: MAX_SAMPLE_SIZE }));
app.use(express.static(path.join(__dirname, 'build'))); app.use(express.static(path.join(__dirname, "build")));
// Dev mode request logging middleware
if (devMode) {
app.use((req, res, next) => {
const timestamp = new Date().toISOString();
console.log(`📨 [${timestamp}] ${req.method} ${req.path}`);
if (req.method !== "GET" && Object.keys(req.body).length > 0) {
const bodySize = Buffer.byteLength(JSON.stringify(req.body), "utf8");
console.log(` Request body size: ${(bodySize / 1024).toFixed(2)}KB`);
}
const originalJson = res.json;
res.json = function (data) {
console.log(` ✓ Response: ${res.statusCode}`);
return originalJson.call(this, data);
};
next();
});
}
// Session storage // Session storage
const sessions = new Map(); const sessions = new Map();
@@ -127,7 +124,9 @@ function createApp() {
for (const [sessionId, session] of sessions.entries()) { for (const [sessionId, session] of sessions.entries()) {
if (now - session.createdAt > MAX_SESSION_TTL) { if (now - session.createdAt > MAX_SESSION_TTL) {
sessions.delete(sessionId); sessions.delete(sessionId);
console.log(`🧹 Cleaned up expired session: ${sessionId.substring(0, 8)}...`); console.log(
`🧹 Cleaned up expired session: ${sessionId.substring(0, 8)}...`,
);
} }
} }
} }
@@ -136,22 +135,15 @@ function createApp() {
setInterval(cleanupExpiredSessions, 5 * 60 * 1000); setInterval(cleanupExpiredSessions, 5 * 60 * 1000);
// API endpoints // API endpoints
app.post('/api/v1/upload', (req, res) => { app.post("/api/v1/upload", (req, res) => {
try { try {
// Check if request is from localhost - if so, skip API key validation const apiKey = req.headers["x-api-key"];
const isFromLocalhost = isLocalhostRequest(req);
let apiKey = req.headers['x-api-key'];
if (!isFromLocalhost) { // Validate API key header
// Validate API key header for remote clients
if (!apiKey || !isValidApiKey(apiKey)) { if (!apiKey || !isValidApiKey(apiKey)) {
return res.status(403).json({ error: 'Invalid or missing X-API-Key header' }); return res
} .status(403)
} else { .json({ error: "Invalid or missing X-API-Key header" });
// For localhost requests, use consistent API key for session persistence
if (!apiKey || !isValidApiKey(apiKey)) {
apiKey = LOCALHOST_API_KEY;
}
} }
// Cleanup expired sessions before checking limits // Cleanup expired sessions before checking limits
@@ -160,26 +152,26 @@ function createApp() {
// Check session limits // Check session limits
if (sessions.size >= MAX_SESSIONS) { if (sessions.size >= MAX_SESSIONS) {
return res.status(429).json({ return res.status(429).json({
error: 'Maximum number of sessions reached. Please try again later.', error: "Maximum number of sessions reached. Please try again later.",
maxSessions: MAX_SESSIONS, maxSessions: MAX_SESSIONS,
currentSessions: sessions.size currentSessions: sessions.size,
}); });
} }
const uploadedData = req.body; const uploadedData = req.body;
// Validate that it's valid JSON // Validate that it's valid JSON
if (!uploadedData || typeof uploadedData !== 'object') { if (!uploadedData || typeof uploadedData !== "object") {
return res.status(400).json({ error: 'Invalid JSON data' }); return res.status(400).json({ error: "Invalid JSON data" });
} }
// Check data size // Check data size
const dataSize = Buffer.byteLength(JSON.stringify(uploadedData), 'utf8'); const dataSize = Buffer.byteLength(JSON.stringify(uploadedData), "utf8");
if (dataSize > MAX_SAMPLE_SIZE) { if (dataSize > MAX_SAMPLE_SIZE) {
return res.status(413).json({ return res.status(413).json({
error: 'Sample data too large', error: "Sample data too large",
maxSize: MAX_SAMPLE_SIZE, maxSize: MAX_SAMPLE_SIZE,
receivedSize: dataSize receivedSize: dataSize,
}); });
} }
@@ -192,69 +184,66 @@ function createApp() {
const encryptedData = encrypt(uploadedData, key); const encryptedData = encrypt(uploadedData, key);
sessions.set(sessionId, { sessions.set(sessionId, {
salt: salt.toString('hex'), salt: salt.toString("hex"),
encryptedData, encryptedData,
state: stateGuid, state: stateGuid,
createdAt: Date.now(), createdAt: Date.now(),
accessed: false accessed: false,
}); });
console.log(`📁 Session created: ${sessionId.substring(0, 8)}... (${sessions.size}/${MAX_SESSIONS})`); console.log(
`📁 Session created: ${sessionId.substring(0, 8)}... (${sessions.size}/${MAX_SESSIONS})`,
);
res.json({ res.json({
message: 'Sample data uploaded successfully', message: "Sample data uploaded successfully",
state: stateGuid, state: stateGuid,
sessionId: sessionId.substring(0, 8) + '...' sessionId: sessionId.substring(0, 8) + "...",
}); });
} catch (error) { } catch (error) {
console.error('⚠️ Upload endpoint exception occurred:', { console.error("⚠️ Upload endpoint exception occurred:", {
message: error.message, message: error.message,
stack: error.stack, stack: error.stack,
sessionCount: sessions.size, sessionCount: sessions.size,
timestamp: new Date().toISOString() timestamp: new Date().toISOString(),
}); });
// Provide more specific error messages based on error type // Provide more specific error messages based on error type
if (error.name === 'SyntaxError') { if (error.name === "SyntaxError") {
return res.status(400).json({ return res.status(400).json({
error: 'Invalid JSON data format', error: "Invalid JSON data format",
details: 'The uploaded data could not be parsed as valid JSON' details: "The uploaded data could not be parsed as valid JSON",
}); });
} else if (error.message.includes('encrypt')) { } else if (error.message.includes("encrypt")) {
return res.status(500).json({ return res.status(500).json({
error: 'Encryption failed', error: "Encryption failed",
details: 'Failed to encrypt session data. Please try again with a new API key.' details:
"Failed to encrypt session data. Please try again with a new API key.",
}); });
} else if (error.message.includes('PBKDF2')) { } else if (error.message.includes("PBKDF2")) {
return res.status(500).json({ return res.status(500).json({
error: 'Key derivation failed', error: "Key derivation failed",
details: 'Failed to derive encryption key from API key' details: "Failed to derive encryption key from API key",
}); });
} else { } else {
return res.status(500).json({ return res.status(500).json({
error: 'Upload processing failed', error: "Upload processing failed",
details: 'An unexpected error occurred while processing your upload. Please try again.' details:
"An unexpected error occurred while processing your upload. Please try again.",
}); });
} }
} }
}); });
app.get('/api/v1/sample', (req, res) => { app.get("/api/v1/sample", (req, res) => {
try { try {
// Check if request is from localhost - if so, skip API key validation const apiKey = req.headers["x-api-key"];
const isFromLocalhost = isLocalhostRequest(req);
let apiKey = req.headers['x-api-key'];
if (!isFromLocalhost) { // Validate API key header
// Validate API key header for remote clients
if (!apiKey || !isValidApiKey(apiKey)) { if (!apiKey || !isValidApiKey(apiKey)) {
return res.status(403).json({ error: 'Invalid or missing X-API-Key header' }); return res
} .status(403)
} else { .json({ error: "Invalid or missing X-API-Key header" });
// For localhost requests, use consistent API key for session persistence
if (!apiKey || !isValidApiKey(apiKey)) {
apiKey = LOCALHOST_API_KEY;
}
} }
const sessionId = getSessionId(apiKey); const sessionId = getSessionId(apiKey);
@@ -265,64 +254,62 @@ function createApp() {
} }
// Decrypt data // Decrypt data
const salt = Buffer.from(session.salt, 'hex'); const salt = Buffer.from(session.salt, "hex");
const key = deriveKey(apiKey, salt); const key = deriveKey(apiKey, salt);
const decryptedData = decrypt(session.encryptedData, key); const decryptedData = decrypt(session.encryptedData, key);
// Remove session after first access (one-time use) // Remove session after first access (one-time use)
sessions.delete(sessionId); sessions.delete(sessionId);
console.log(`📤 Sample data retrieved and session cleared: ${sessionId.substring(0, 8)}...`); console.log(
`📤 Sample data retrieved and session cleared: ${sessionId.substring(0, 8)}...`,
);
res.json(decryptedData); res.json(decryptedData);
} catch (error) { } catch (error) {
console.error('⚠️ Sample retrieval exception occurred:', { console.error("⚠️ Sample retrieval exception occurred:", {
message: error.message, message: error.message,
stack: error.stack, stack: error.stack,
sessionCount: sessions.size, sessionCount: sessions.size,
timestamp: new Date().toISOString() timestamp: new Date().toISOString(),
}); });
// Provide more specific error messages based on error type // Provide more specific error messages based on error type
if (error.message.includes('decrypt')) { if (error.message.includes("decrypt")) {
return res.status(500).json({ return res.status(500).json({
error: 'Decryption failed', error: "Decryption failed",
details: 'Failed to decrypt session data. The session may be corrupted or the API key may be incorrect.' details:
"Failed to decrypt session data. The session may be corrupted or the API key may be incorrect.",
}); });
} else if (error.message.includes('JSON')) { } else if (error.message.includes("JSON")) {
return res.status(500).json({ return res.status(500).json({
error: 'Data corruption detected', error: "Data corruption detected",
details: 'The stored session data appears to be corrupted and cannot be parsed.' details:
"The stored session data appears to be corrupted and cannot be parsed.",
}); });
} else if (error.name === 'TypeError') { } else if (error.name === "TypeError") {
return res.status(500).json({ return res.status(500).json({
error: 'Session data format error', error: "Session data format error",
details: 'The session data format is invalid or corrupted.' details: "The session data format is invalid or corrupted.",
}); });
} else { } else {
return res.status(500).json({ return res.status(500).json({
error: 'Sample retrieval failed', error: "Sample retrieval failed",
details: 'An unexpected error occurred while retrieving sample data. The session may have been corrupted.' details:
"An unexpected error occurred while retrieving sample data. The session may have been corrupted.",
}); });
} }
} }
}); });
app.get('/api/v1/state', (req, res) => { app.get("/api/v1/state", (req, res) => {
try { try {
// Check if request is from localhost - if so, skip API key validation const apiKey = req.headers["x-api-key"];
const isFromLocalhost = isLocalhostRequest(req);
let apiKey = req.headers['x-api-key'];
if (!isFromLocalhost) { // Validate API key header
// Validate API key header for remote clients
if (!apiKey || !isValidApiKey(apiKey)) { if (!apiKey || !isValidApiKey(apiKey)) {
return res.status(403).json({ error: 'Invalid or missing X-API-Key header' }); return res
} .status(403)
} else { .json({ error: "Invalid or missing X-API-Key header" });
// For localhost requests, use consistent API key for session persistence
if (!apiKey || !isValidApiKey(apiKey)) {
apiKey = LOCALHOST_API_KEY;
}
} }
const sessionId = getSessionId(apiKey); const sessionId = getSessionId(apiKey);
@@ -335,50 +322,56 @@ function createApp() {
res.json({ state: session.state }); res.json({ state: session.state });
} catch (error) { } catch (error) {
console.error('⚠️ State retrieval exception occurred:', { console.error("⚠️ State retrieval exception occurred:", {
message: error.message, message: error.message,
stack: error.stack, stack: error.stack,
sessionCount: sessions.size, sessionCount: sessions.size,
timestamp: new Date().toISOString() timestamp: new Date().toISOString(),
}); });
// Provide more specific error messages // Provide more specific error messages
if (error.message.includes('API key')) { if (error.message.includes("API key")) {
return res.status(403).json({ return res.status(403).json({
error: 'API key processing failed', error: "API key processing failed",
details: 'Failed to process the provided API key' details: "Failed to process the provided API key",
}); });
} else { } else {
return res.status(500).json({ return res.status(500).json({
error: 'State retrieval failed', error: "State retrieval failed",
details: 'An unexpected error occurred while retrieving session state. Please try again.' details:
"An unexpected error occurred while retrieving session state. Please try again.",
}); });
} }
} }
}); });
// Health endpoint (no auth required) // Status endpoint (no auth required) - detailed information
app.get('/api/v1/health', (req, res) => { app.get("/api/v1/status", (req, res) => {
cleanupExpiredSessions(); // Cleanup on health check cleanupExpiredSessions(); // Cleanup on status check
res.json({ res.json({
status: 'healthy', status: "healthy",
sessions: { sessions: {
current: sessions.size, current: sessions.size,
max: MAX_SESSIONS, max: MAX_SESSIONS,
available: MAX_SESSIONS - sessions.size available: MAX_SESSIONS - sessions.size,
}, },
limits: { limits: {
maxSessions: MAX_SESSIONS, maxSessions: MAX_SESSIONS,
maxSampleSize: MAX_SAMPLE_SIZE, maxSampleSize: MAX_SAMPLE_SIZE,
maxSessionTTL: MAX_SESSION_TTL maxSessionTTL: MAX_SESSION_TTL,
}, },
uptime: process.uptime() uptime: process.uptime(),
}); });
}); });
// Health endpoint (no auth required) - simple OK response
app.get("/api/v1/health", (req, res) => {
res.type("text/plain").send("OK");
});
// Serve React app for all other routes // Serve React app for all other routes
app.get('*', (req, res) => { app.get("*", (req, res) => {
res.sendFile(path.join(__dirname, 'build', 'index.html')); res.sendFile(path.join(__dirname, "build", "index.html"));
}); });
return app; return app;
@@ -386,37 +379,84 @@ function createApp() {
// Start server if this file is run directly // Start server if this file is run directly
if (require.main === module) { if (require.main === module) {
// Parse command line arguments const { values } = parseArgs({
const args = process.argv.slice(2); options: {
let listenAddr = process.env.LISTEN_ADDR || '127.0.0.1'; "listen-addr": {
let listenPort = process.env.LISTEN_PORT || 3000; type: "string",
short: "h",
default: process.env.LISTEN_ADDR || "127.0.0.1",
},
port: {
type: "string",
short: "p",
default: process.env.LISTEN_PORT || "3000",
},
dev: {
type: "boolean",
default: process.env.DEV_MODE === "true" || false,
},
},
});
for (let i = 0; i < args.length; i++) { const DEV_MODE = values.dev;
if (args[i] === '-h' || args[i] === '--listen-addr') { const app = createApp(DEV_MODE);
listenAddr = args[i + 1]; const PORT = parseInt(values.port);
i++; const HOST = values["listen-addr"];
} else if (args[i] === '-p' || args[i] === '--port') {
listenPort = args[i + 1];
i++;
}
}
const app = createApp();
const PORT = parseInt(listenPort);
const HOST = listenAddr;
app.listen(PORT, HOST, () => { app.listen(PORT, HOST, () => {
console.log(`🚀 JMESPath Playground Server running on http://${HOST}:${PORT}`); console.log(`JMESPath Playground Server running`);
console.log(`📊 Configuration:`); if (DEV_MODE) {
console.log(` 🔧 Development Mode Enabled`);
}
// Show actual accessible URLs
if (HOST === "0.0.0.0") {
console.log(` Listening on all interfaces:`);
const interfaces = os.networkInterfaces();
for (const [name, addrs] of Object.entries(interfaces)) {
for (const addr of addrs) {
if (addr.family === "IPv4" && !addr.internal) {
console.log(` http://${addr.address}:${PORT}`);
}
}
}
// Also show localhost for local access
console.log(` http://127.0.0.1:${PORT}`);
} else {
console.log(` http://${HOST}:${PORT}`);
}
console.log(`Configuration:`);
console.log(` Max Sessions: ${MAX_SESSIONS}`); console.log(` Max Sessions: ${MAX_SESSIONS}`);
console.log(` Max Sample Size: ${(MAX_SAMPLE_SIZE / 1024 / 1024).toFixed(1)}MB`); console.log(
console.log(` Session TTL: ${(MAX_SESSION_TTL / 1000 / 60).toFixed(0)} minutes`); ` Max Sample Size: ${(MAX_SAMPLE_SIZE / 1024 / 1024).toFixed(1)}MB`,
console.log(`🔗 API endpoints:`); );
console.log(` POST http://${HOST}:${PORT}/api/v1/upload (requires X-API-Key)`); console.log(
console.log(` GET http://${HOST}:${PORT}/api/v1/sample (requires X-API-Key)`); ` Session TTL: ${(MAX_SESSION_TTL / 1000 / 60).toFixed(0)} minutes`,
console.log(` GET http://${HOST}:${PORT}/api/v1/state (requires X-API-Key)`); );
console.log(` GET http://${HOST}:${PORT}/api/v1/health (public)`); console.log(
console.log(`🔐 Security: AES-256-GCM encryption with PBKDF2 key derivation`); " Security: AES-256-GCM encryption with PBKDF2 (100k iterations)",
);
// Show base API URL
let apiBaseUrl;
if (HOST === "0.0.0.0") {
const interfaces = os.networkInterfaces();
let firstIP = "127.0.0.1";
outer: for (const addrs of Object.values(interfaces)) {
for (const addr of addrs) {
if (addr.family === "IPv4" && !addr.internal) {
firstIP = addr.address;
break outer;
}
}
}
apiBaseUrl = `http://${firstIP}:${PORT}/api/v1`;
} else {
apiBaseUrl = `http://${HOST}:${PORT}/api/v1`;
}
console.log(`API Base URL: ${apiBaseUrl}`);
}); });
} }

View File

@@ -5,6 +5,22 @@
--font-mono: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace; --font-mono: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
--accent-color: #007bff; --accent-color: #007bff;
/* Brand colors */
--brand-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
--brand-white: #ffffff;
--brand-dark: #212529;
--brand-warning: #ffc107;
/* Brand opacity levels */
--brand-white-60: rgba(255, 255, 255, 0.6);
--brand-white-10: rgba(255, 255, 255, 0.1);
--brand-warning-50: rgba(255, 193, 7, 0.5);
--brand-warning-10: rgba(255, 193, 7, 0.1);
/* Elevation and overlays */
--shadow-light: rgba(0, 0, 0, 0.1);
--focus-ring: rgba(0, 123, 255, 0.25);
/* Button variants */ /* Button variants */
--btn-success: #28a745; --btn-success: #28a745;
--btn-info: #17a2b8; --btn-info: #17a2b8;
@@ -36,23 +52,57 @@ body {
/* Header section styling - more compact */ /* Header section styling - more compact */
.header-section { .header-section {
/* Removed gradient background to fix text visibility */ background: var(--brand-gradient);
color: var(--brand-white);
padding: 1.2rem 0;
margin-bottom: 1rem;
transition: background-color 0.3s ease; transition: background-color 0.3s ease;
} }
.header-section h2 {
color: var(--brand-white);
}
/* Ensure buttons in header are clearly visible against gradient */
.header-section .btn-light.active {
background-color: var(--brand-white);
color: var(--brand-dark) !important; /* Deep dark text for selected states */
border-color: var(--brand-white);
}
.header-section .btn-outline-light {
color: var(--brand-white);
border-color: var(--brand-white-60);
}
.header-section .btn-outline-light:hover {
background-color: var(--brand-white-10);
color: var(--brand-white);
}
.header-section .btn-outline-warning {
color: var(--brand-warning);
border-color: var(--brand-warning-50);
}
.header-section .btn-outline-warning:hover {
background-color: var(--brand-warning-10);
color: var(--brand-warning);
}
/* Custom card styling */ /* Custom card styling */
.card { .card {
border: none; border: none;
box-shadow: 0 2px 8px rgba(0,0,0,0.1); box-shadow: 0 2px 8px var(--shadow-light);
border-radius: 8px; border-radius: 8px;
transition: background-color 0.3s ease, box-shadow 0.3s ease; transition: background-color 0.3s ease, box-shadow 0.3s ease;
} }
.card-header { .card-header {
background-color: #f8f9fa; background-color: var(--bg-secondary);
border-bottom: 2px solid #dee2e6; border-bottom: 2px solid var(--border);
font-weight: 600; font-weight: 600;
color: #212529; color: var(--text-primary);
transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease; transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease;
} }
@@ -224,7 +274,7 @@ footer a:hover {
/* Focus states */ /* Focus states */
.jmespath-input:focus { .jmespath-input:focus {
border-color: var(--accent-color, #007bff); border-color: var(--accent-color, #007bff);
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); box-shadow: 0 0 0 0.2rem var(--focus-ring);
} }
.json-input:focus, .json-input:focus,
@@ -232,7 +282,7 @@ footer a:hover {
background-color: var(--bg-primary); background-color: var(--bg-primary);
border-color: var(--accent-color, #007bff); border-color: var(--accent-color, #007bff);
color: var(--text-secondary); color: var(--text-secondary);
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); box-shadow: 0 0 0 0.2rem var(--focus-ring);
} }
/* Placeholder colors */ /* Placeholder colors */
@@ -249,6 +299,12 @@ footer a:hover {
color: var(--error-text); color: var(--error-text);
} }
.alert-success {
background-color: var(--success-bg);
border-color: var(--success-border);
color: var(--success-text);
}
/* Code block styles */ /* Code block styles */
pre.bg-light { pre.bg-light {
background-color: var(--bg-secondary) !important; background-color: var(--bg-secondary) !important;

View File

@@ -1,16 +1,16 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from "react";
import Header from './components/Header'; import Header from "./components/Header";
import Footer from './components/Footer'; import Footer from "./components/Footer";
import MainPage from './components/MainPage'; import MainPage from "./components/MainPage";
import ApiKeyPage from './components/ApiKeyPage'; import ApiKeyPage from "./components/ApiKeyPage";
import './App.css'; import "./App.css";
// Utility function to generate a cryptographically secure API key // Utility function to generate a cryptographically secure API key
function generateApiKey() { function generateApiKey() {
const array = new Uint8Array(16); const array = new Uint8Array(16);
// Use crypto.getRandomValues if available (browser), fallback for tests // Use crypto.getRandomValues if available (browser), fallback for tests
if (typeof crypto !== 'undefined' && crypto.getRandomValues) { if (typeof crypto !== "undefined" && crypto.getRandomValues) {
crypto.getRandomValues(array); crypto.getRandomValues(array);
} else { } else {
// Fallback for test environments - not cryptographically secure // Fallback for test environments - not cryptographically secure
@@ -19,62 +19,83 @@ function generateApiKey() {
} }
} }
return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join(''); return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join(
"",
);
} }
// JMESPath Testing Tool - Main Application Component // JMESPath Testing Tool - Main Application Component
function App() { function App() {
const [currentPage, setCurrentPage] = useState('main'); // 'main' or 'apikey' const [currentPage, setCurrentPage] = useState("main"); // 'main' or 'apikey'
const [theme, setTheme] = useState(() => { const [theme, setTheme] = useState(() => {
// Load theme from localStorage or default to 'auto' // Load theme from localStorage or default to 'auto'
return localStorage.getItem('theme') || 'auto'; return localStorage.getItem("theme") || "auto";
});
const [shellType, setShellType] = useState(() => {
// Load shell type from localStorage or default to 'bash'
return localStorage.getItem("jmespath-shell-type") || "bash";
}); });
const [showReloadButton, setShowReloadButton] = useState(false); const [showReloadButton, setShowReloadButton] = useState(false);
const [currentStateGuid, setCurrentStateGuid] = useState(null); const [currentStateGuid, setCurrentStateGuid] = useState(null);
const [sampleData, setSampleData] = useState(null); const [jmespathExpression, setJmespathExpression] =
useState("people[0].name");
const [jsonData, setJsonData] = useState(`{
"people": [
{
"name": "John Doe",
"age": 30,
"city": "New York"
},
{
"name": "Jane Smith",
"age": 25,
"city": "Los Angeles"
}
],
"total": 2
}`);
const [apiKey, setApiKey] = useState(() => { const [apiKey, setApiKey] = useState(() => {
// Load API key from localStorage or generate new one // Load API key from localStorage or generate new one
const stored = localStorage.getItem('jmespath-api-key'); const stored = localStorage.getItem("jmespath-api-key");
if (stored && /^[0-9a-f]{32}$/i.test(stored)) { if (stored && /^[0-9a-f]{32}$/i.test(stored)) {
return stored; return stored;
} }
const newKey = generateApiKey(); const newKey = generateApiKey();
localStorage.setItem('jmespath-api-key', newKey); localStorage.setItem("jmespath-api-key", newKey);
return newKey; return newKey;
}); });
// Theme management // Theme management
useEffect(() => { useEffect(() => {
const applyTheme = (selectedTheme) => { const applyTheme = (selectedTheme) => {
const effectiveTheme = selectedTheme === 'auto' const effectiveTheme =
? (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') selectedTheme === "auto"
? window.matchMedia &&
window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light"
: selectedTheme; : selectedTheme;
document.documentElement.setAttribute('data-bs-theme', effectiveTheme); document.documentElement.setAttribute("data-bs-theme", effectiveTheme);
}; };
applyTheme(theme); applyTheme(theme);
// Save theme preference // Save theme preference
localStorage.setItem('theme', theme); localStorage.setItem("theme", theme);
}, [theme]); }, [theme]);
// Shell type management
useEffect(() => {
localStorage.setItem("jmespath-shell-type", shellType);
}, [shellType]);
// Get headers for API requests // Get headers for API requests
const getApiHeaders = () => { const getApiHeaders = () => {
const headers = { return {
'Accept': 'application/json' Accept: "application/json",
"X-API-Key": apiKey,
}; };
// Only send API key for non-localhost requests
// For localhost, let server use its default LOCALHOST_API_KEY
if (window.location.hostname !== 'localhost' &&
window.location.hostname !== '127.0.0.1' &&
!window.location.hostname.startsWith('127.') &&
window.location.hostname !== '::1') {
headers['X-API-Key'] = apiKey;
}
return headers;
}; };
// Load sample data from API on startup and setup periodic state checking // Load sample data from API on startup and setup periodic state checking
@@ -90,8 +111,8 @@ function App() {
// Check if state has changed (new data uploaded) // Check if state has changed (new data uploaded)
const checkStateChange = async () => { const checkStateChange = async () => {
try { try {
const response = await fetch('/api/v1/state', { const response = await fetch("/api/v1/state", {
headers: getApiHeaders() headers: getApiHeaders(),
}); });
if (response.ok) { if (response.ok) {
@@ -109,19 +130,19 @@ function App() {
const loadSampleData = async () => { const loadSampleData = async () => {
try { try {
setShowReloadButton(false); setShowReloadButton(false);
const response = await fetch('/api/v1/sample', { const response = await fetch("/api/v1/sample", {
headers: getApiHeaders() headers: getApiHeaders(),
}); });
if (response.ok) { if (response.ok) {
const data = await response.json(); const data = await response.json();
if (data) { if (data) {
setSampleData(data); setJsonData(JSON.stringify(data, null, 2));
} }
// Update current state GUID // Update current state GUID
const stateResponse = await fetch('/api/v1/state', { const stateResponse = await fetch("/api/v1/state", {
headers: getApiHeaders() headers: getApiHeaders(),
}); });
if (stateResponse.ok) { if (stateResponse.ok) {
const stateData = await stateResponse.json(); const stateData = await stateResponse.json();
@@ -129,7 +150,7 @@ function App() {
} }
} }
} catch (error) { } catch (error) {
console.error('Failed to load sample data:', error); console.error("Failed to load sample data:", error);
} }
}; };
@@ -137,7 +158,7 @@ function App() {
const regenerateApiKey = () => { const regenerateApiKey = () => {
const newKey = generateApiKey(); const newKey = generateApiKey();
setApiKey(newKey); setApiKey(newKey);
localStorage.setItem('jmespath-api-key', newKey); localStorage.setItem("jmespath-api-key", newKey);
setShowReloadButton(false); setShowReloadButton(false);
setCurrentStateGuid(null); setCurrentStateGuid(null);
}; };
@@ -151,7 +172,7 @@ function App() {
}; };
return ( return (
<div className="container-fluid vh-100 d-flex flex-column"> <div className="vh-100 d-flex flex-column">
<Header <Header
theme={theme} theme={theme}
onThemeChange={handleThemeChange} onThemeChange={handleThemeChange}
@@ -160,18 +181,26 @@ function App() {
/> />
{/* Main Content Section - flex-grow to fill space */} {/* Main Content Section - flex-grow to fill space */}
<div className="container-fluid flex-grow-1 d-flex flex-column" style={{ minHeight: 0 }}> <div
{currentPage === 'main' ? ( className="container-fluid flex-grow-1 d-flex flex-column"
style={{ minHeight: 0 }}
>
{currentPage === "main" ? (
<MainPage <MainPage
apiKey={apiKey} apiKey={apiKey}
showReloadButton={showReloadButton} showReloadButton={showReloadButton}
onReloadSampleData={loadSampleData} onReloadSampleData={loadSampleData}
initialSampleData={sampleData} jmespathExpression={jmespathExpression}
setJmespathExpression={setJmespathExpression}
jsonData={jsonData}
setJsonData={setJsonData}
/> />
) : ( ) : (
<ApiKeyPage <ApiKeyPage
apiKey={apiKey} apiKey={apiKey}
onRegenerateApiKey={regenerateApiKey} onRegenerateApiKey={regenerateApiKey}
shellType={shellType}
onShellTypeChange={setShellType}
/> />
)} )}
</div> </div>

View File

@@ -1,13 +1,27 @@
import { render, screen, fireEvent, waitFor } from '@testing-library/react'; import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import App from './App'; import App from './App';
import { vi } from 'vitest';
import '@testing-library/jest-dom';
// Mock localStorage
const localStorageMock = (function() {
let store = {};
return {
getItem: vi.fn((key) => store[key] || null),
setItem: vi.fn((key, value) => { store[key] = value.toString(); }),
clear: vi.fn(() => { store = {}; }),
removeItem: vi.fn((key) => { delete store[key]; })
};
})();
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
// Mock fetch for API calls // Mock fetch for API calls
global.fetch = jest.fn(); global.fetch = vi.fn();
describe('App Component', () => { describe('App Component', () => {
beforeEach(() => { beforeEach(() => {
fetch.mockClear(); vi.clearAllMocks();
// Mock successful API responses // Mock successful API responses
fetch.mockImplementation((url) => { fetch.mockImplementation((url) => {
if (url.includes('/api/v1/sample')) { if (url.includes('/api/v1/sample')) {
@@ -55,16 +69,19 @@ describe('App Component', () => {
test('renders version number', () => { test('renders version number', () => {
render(<App />); render(<App />);
const versionText = screen.getByText(/v\d+\.\d+\.\d+(-dev|-test)?/); // Version can be either v1.2.3 format (release), v1.2.3-dev/test format (legacy dev), or "unknown" format (new dev)
const versionText = screen.getByText(/(v\d+\.\d+\.\d+(-dev|-test)?|unknown)/);
expect(versionText).toBeInTheDocument(); expect(versionText).toBeInTheDocument();
// Check if it's a dev/test build // Check if it's a dev/test/unknown build
const isDevBuild = versionText.textContent.includes('-dev') || versionText.textContent.includes('-test'); const isDevBuild = versionText.textContent.includes('-dev') ||
versionText.textContent.includes('-test') ||
versionText.textContent.includes('unknown');
// Additional validations can be added here based on build type // Additional validations can be added here based on build type
if (isDevBuild) { if (isDevBuild) {
// Dev/test specific validations could go here // Dev/test/unknown specific validations
expect(versionText.textContent).toMatch(/v\d+\.\d+\.\d+-(dev|test)/); expect(versionText.textContent).toMatch(/(v\d+\.\d+\.\d+-(dev|test)|unknown)/);
} else { } else {
// Release build validations - just check that version pattern exists in the text // Release build validations - just check that version pattern exists in the text
expect(versionText.textContent).toMatch(/v\d+\.\d+\.\d+/); expect(versionText.textContent).toMatch(/v\d+\.\d+\.\d+/);

View File

@@ -1,102 +0,0 @@
import React, { useState } from 'react';
function ApiKeyPage({ apiKey, onRegenerateApiKey }) {
const [copySuccess, setCopySuccess] = useState(false);
const handleCopyToClipboard = async () => {
try {
await navigator.clipboard.writeText(apiKey);
setCopySuccess(true);
setTimeout(() => setCopySuccess(false), 2000);
} catch (err) {
console.error('Failed to copy to clipboard:', err);
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = apiKey;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
setCopySuccess(true);
setTimeout(() => setCopySuccess(false), 2000);
}
};
return (
<div className="row justify-content-center">
<div className="col-md-8">
<div className="card">
<div className="card-header">
<h5 className="mb-0">🔐 API Key Management</h5>
</div>
<div className="card-body">
<div className="mb-4">
<label className="form-label fw-bold">Your API Key:</label>
<div className="input-group">
<input
type="text"
className="form-control font-monospace"
value={apiKey}
readOnly
/>
<button
className={`btn ${copySuccess ? 'btn-success' : 'btn-outline-secondary'}`}
onClick={handleCopyToClipboard}
title="Copy API key to clipboard"
>
{copySuccess ? '✓ Copied!' : '📋 Copy'}
</button>
<button
className="btn btn-outline-primary"
onClick={onRegenerateApiKey}
title="Generate new API key"
>
🔄 Regenerate
</button>
</div>
<div className="form-text">
This API key is used to encrypt and authenticate data uploads from remote clients.
<strong>Note:</strong> Requests from localhost (127.0.0.1) do not require an API key.
</div>
</div>
<div className="mb-4">
<h6>📡 Remote Data Upload API</h6>
<p className="text-muted">
External tools can upload sample data remotely using the REST API.
For remote clients, the API key is required for authentication:
</p>
<pre className="bg-light p-3 rounded border">
<code>{`curl -s -X POST \\
-H "Content-Type: application/json" \\
-H "Accept: application/json" \\
-H "X-API-Key: ${apiKey}" \\
--data @{{JSON_FILE_NAME}} \\
"${window.location.origin}/api/v1/upload"`}</code>
</pre>
<div className="form-text">
Replace <code>{'{{JSON_FILE_NAME}}'}</code> with the path to your JSON file containing the sample data.
<br />
<strong>For localhost clients:</strong> The X-API-Key header is optional and can be omitted.
</div>
</div>
<div className="alert alert-info">
<h6 className="alert-heading"> How it works:</h6>
<ul className="mb-0">
<li>Remote clients require API key authentication for security</li>
<li>Localhost clients (127.0.0.1) can access the API without authentication</li>
<li>Your data is encrypted using AES-256-GCM with PBKDF2 key derivation</li>
<li>Data is automatically cleared after first retrieval (one-time use)</li>
<li>Sessions expire after 1 hour for security</li>
<li>Maximum 100 concurrent sessions supported</li>
</ul>
</div>
</div>
</div>
</div>
</div>
);
}
export default ApiKeyPage;

View File

@@ -0,0 +1,146 @@
import React, { useState } from 'react';
function CodeBlock({ code }) {
const [copySuccess, setCopySuccess] = useState(false);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(code);
setCopySuccess(true);
setTimeout(() => setCopySuccess(false), 2000);
} catch (err) {
console.error('Failed to copy to clipboard:', err);
}
};
return (
<div className="position-relative">
<pre className="bg-light p-3 pe-5 rounded border shadow-sm">
<code className="d-block" style={{ whiteSpace: 'pre-wrap' }}>{code}</code>
</pre>
<button
className={`btn btn-sm ${copySuccess ? 'btn-success' : 'btn-outline-secondary'} position-absolute top-0 end-0 m-2`}
onClick={handleCopy}
title="Copy code to clipboard"
style={{ opacity: 0.8 }}
>
{copySuccess ? '✓' : '📋'}
</button>
</div>
);
}
function ApiKeyPage({ apiKey, onRegenerateApiKey, shellType, onShellTypeChange }) {
const [copySuccess, setCopySuccess] = useState(false);
const handleCopyToClipboard = async () => {
try {
await navigator.clipboard.writeText(apiKey);
setCopySuccess(true);
setTimeout(() => setCopySuccess(false), 2000);
} catch (err) {
console.error('Failed to copy to clipboard:', err);
// Fallback for older browsers
const textArea = document.createElement('textarea');
textArea.value = apiKey;
document.body.appendChild(textArea);
textArea.select();
document.execCommand('copy');
document.body.removeChild(textArea);
setCopySuccess(true);
setTimeout(() => setCopySuccess(false), 2000);
}
};
return (
<div className="row justify-content-center">
<div className="col-md-8">
<div className="card">
<div className="card-header">
<h5 className="mb-0">🔐 API Key Management</h5>
</div>
<div className="card-body">
<div className="mb-4">
<label className="form-label fw-bold">Your API Key:</label>
<div className="input-group">
<input
type="text"
className="form-control font-monospace"
value={apiKey}
readOnly
/>
<button
className={`btn ${copySuccess ? 'btn-success' : 'btn-outline-secondary'}`}
onClick={handleCopyToClipboard}
title="Copy API key to clipboard"
>
{copySuccess ? '✓ Copied!' : '📋 Copy'}
</button>
<button
className="btn btn-outline-primary"
onClick={onRegenerateApiKey}
title="Generate new API key"
>
🔄 Regenerate
</button>
</div>
<div className="form-text">
This API key is used to encrypt and authenticate data uploads.
</div>
</div>
<div className="mb-4">
<div className="d-flex justify-content-between align-items-center mb-2">
<h6 className="mb-0">📡 Remote Data Upload API</h6>
<div className="btn-group btn-group-sm" role="group" aria-label="Shell type selector">
<input
type="radio"
className="btn-check"
name="shellType"
id="shellBash"
autoComplete="off"
checked={shellType === 'bash'}
onChange={() => onShellTypeChange('bash')}
/>
<label className="btn btn-outline-secondary" htmlFor="shellBash">UNIX (Bash)</label>
<input
type="radio"
className="btn-check"
name="shellType"
id="shellPowerShell"
autoComplete="off"
checked={shellType === 'powershell'}
onChange={() => onShellTypeChange('powershell')}
/>
<label className="btn btn-outline-secondary" htmlFor="shellPowerShell">Windows (PowerShell)</label>
</div>
</div>
<p className="text-muted">
External tools can upload sample data remotely using the REST API.
The API key is required for authentication. Define two
environment variables in your {shellType === 'bash' ? <code>.bashrc</code> : <code>PowerShell profile</code>}.
</p>
<CodeBlock
code={shellType === 'bash'
? `export JMESPATH_PLAYGROUND_API_URL=${window.location.origin}\nexport JMESPATH_PLAYGROUND_API_KEY=${apiKey}`
: `$env:JMESPATH_PLAYGROUND_API_URL = "${window.location.origin}"\n$env:JMESPATH_PLAYGROUND_API_KEY = "${apiKey}"`}
/>
<p className="text-muted">Then, use the following {shellType === 'bash' ? <code>curl</code> : <code>PowerShell</code>} command to upload your data:</p>
<CodeBlock
code={shellType === 'bash'
? `curl -s -X POST \\\n -H "Content-Type: application/json" \\\n -H "Accept: application/json" \\\n -H "X-API-Key: $JMESPATH_PLAYGROUND_API_KEY" \\\n --data @__JSON_FILE_NAME__ \\\n "$JMESPATH_PLAYGROUND_API_URL/api/v1/upload"`
: `Invoke-RestMethod -Uri "$env:JMESPATH_PLAYGROUND_API_URL/api/v1/upload" \\\n -Method Post \\\n -ContentType "application/json" \\\n -Headers @{\n "X-API-Key" = $env:JMESPATH_PLAYGROUND_API_KEY\n "Accept" = "application/json"\n } \\\n -InFile __JSON_FILE_NAME__`}
/>
<div className="form-text">
Replace <code>{"__JSON_FILE_NAME__"}</code> with the path to your
JSON file containing the sample data. {shellType === 'bash' && <span>or use <code>-</code> to read from standard input.</span>}
</div>
</div>
</div>
</div>
</div>
</div>
);
}
export default ApiKeyPage;

View File

@@ -8,7 +8,7 @@ function Footer() {
<div className="row"> <div className="row">
<div className="col-md-6"> <div className="col-md-6">
<p className="mb-0 text-muted small"> <p className="mb-0 text-muted small">
<strong>JMESPath Testing Tool</strong> v{VERSION} - Created for testing and validating JMESPath expressions <strong>JMESPath Testing Tool</strong> {VERSION === 'unknown' ? VERSION : `v${VERSION}`} - Created for testing and validating JMESPath expressions
</p> </p>
</div> </div>
<div className="col-md-6 text-md-end"> <div className="col-md-6 text-md-end">

View File

@@ -2,19 +2,19 @@ import React from 'react';
function Header({ theme, onThemeChange, currentPage, onPageChange }) { function Header({ theme, onThemeChange, currentPage, onPageChange }) {
return ( return (
<div className="header-section py-2"> <div className="header-section">
<div className="container"> <div className="container-fluid px-4">
<div className="row"> <div className="row align-items-center">
<div className="col-12 text-center position-relative"> <div className="col-12 text-center position-relative">
<h2 className="mb-1">JMESPath Testing Tool</h2> <h2 className="mb-1">JMESPath Testing Tool</h2>
{/* Right side controls - better positioning */} {/* Right side controls - better positioning */}
<div className="position-absolute top-0 end-0 d-flex align-items-center gap-2"> <div className="position-absolute top-50 end-0 translate-middle-y d-flex align-items-center gap-2 me-4">
{/* API Key Management Button - more prominent */} {/* API Key Management Button - more prominent */}
<button <button
type="button" type="button"
className={`btn btn-sm ${ className={`btn btn-sm ${
currentPage === 'apikey' currentPage === 'apikey'
? 'btn-warning fw-bold' ? 'btn-warning fw-bold text-dark'
: 'btn-outline-warning' : 'btn-outline-warning'
}`} }`}
onClick={() => onPageChange(currentPage === 'main' ? 'apikey' : 'main')} onClick={() => onPageChange(currentPage === 'main' ? 'apikey' : 'main')}
@@ -28,8 +28,8 @@ function Header({ theme, onThemeChange, currentPage, onPageChange }) {
type="button" type="button"
className={`btn ${ className={`btn ${
theme === 'auto' theme === 'auto'
? 'btn-primary' ? 'btn-light active'
: 'btn-outline-secondary' : 'btn-outline-light'
}`} }`}
onClick={() => onThemeChange('auto')} onClick={() => onThemeChange('auto')}
title="Auto (follow system)" title="Auto (follow system)"
@@ -40,8 +40,8 @@ function Header({ theme, onThemeChange, currentPage, onPageChange }) {
type="button" type="button"
className={`btn ${ className={`btn ${
theme === 'light' theme === 'light'
? 'btn-primary' ? 'btn-light active'
: 'btn-outline-secondary' : 'btn-outline-light'
}`} }`}
onClick={() => onThemeChange('light')} onClick={() => onThemeChange('light')}
title="Light theme" title="Light theme"
@@ -52,8 +52,8 @@ function Header({ theme, onThemeChange, currentPage, onPageChange }) {
type="button" type="button"
className={`btn ${ className={`btn ${
theme === 'dark' theme === 'dark'
? 'btn-primary' ? 'btn-light active'
: 'btn-outline-secondary' : 'btn-outline-light'
}`} }`}
onClick={() => onThemeChange('dark')} onClick={() => onThemeChange('dark')}
title="Dark theme" title="Dark theme"

View File

@@ -1,41 +1,24 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from "react";
import jmespath from 'jmespath'; import jmespath from "jmespath";
function MainPage({ apiKey, showReloadButton, onReloadSampleData, initialSampleData }) {
const [jmespathExpression, setJmespathExpression] = useState('people[0].name');
const [jsonData, setJsonData] = useState(`{
"people": [
{
"name": "John Doe",
"age": 30,
"city": "New York"
},
{
"name": "Jane Smith",
"age": 25,
"city": "Los Angeles"
}
],
"total": 2
}`);
const [result, setResult] = useState('');
const [error, setError] = useState('');
const [jsonError, setJsonError] = useState('');
// Use initial sample data when provided
useEffect(() => {
if (initialSampleData) {
setJsonData(JSON.stringify(initialSampleData, null, 2));
}
}, [initialSampleData]);
function MainPage({
showReloadButton,
onReloadSampleData,
jmespathExpression,
setJmespathExpression,
jsonData,
setJsonData,
}) {
const [result, setResult] = useState("");
const [error, setError] = useState("");
const [jsonError, setJsonError] = useState("");
const [copySuccess, setCopySuccess] = useState(false);
const evaluateExpression = () => { const evaluateExpression = () => {
try { try {
// Clear previous errors // Clear previous errors
setError(''); setError("");
setJsonError(''); setJsonError("");
// Validate and parse JSON // Validate and parse JSON
let parsedData; let parsedData;
@@ -43,7 +26,7 @@ function MainPage({ apiKey, showReloadButton, onReloadSampleData, initialSampleD
parsedData = JSON.parse(jsonData); parsedData = JSON.parse(jsonData);
} catch (jsonErr) { } catch (jsonErr) {
setJsonError(`Invalid JSON: ${jsonErr.message}`); setJsonError(`Invalid JSON: ${jsonErr.message}`);
setResult(''); setResult("");
return; return;
} }
@@ -52,13 +35,13 @@ function MainPage({ apiKey, showReloadButton, onReloadSampleData, initialSampleD
// Format the result // Format the result
if (queryResult === null || queryResult === undefined) { if (queryResult === null || queryResult === undefined) {
setResult('null'); setResult("null");
} else { } else {
setResult(JSON.stringify(queryResult, null, 2)); setResult(JSON.stringify(queryResult, null, 2));
} }
} catch (jmesErr) { } catch (jmesErr) {
setError(`JMESPath Error: ${jmesErr.message}`); setError(`JMESPath Error: ${jmesErr.message}`);
setResult(''); setResult("");
} }
}; };
@@ -88,30 +71,50 @@ function MainPage({ apiKey, showReloadButton, onReloadSampleData, initialSampleD
}; };
const clearAll = () => { const clearAll = () => {
setJmespathExpression(''); setJmespathExpression("");
setJsonData(''); setJsonData("");
setResult(''); setResult("");
setError(''); setError("");
setJsonError(''); setJsonError("");
};
const copyToClipboard = async () => {
try {
await navigator.clipboard.writeText(result);
setCopySuccess(true);
setTimeout(() => setCopySuccess(false), 2000);
} catch (err) {
console.error("Failed to copy!", err);
}
};
const downloadResult = () => {
const blob = new Blob([result], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "result.json";
a.click();
URL.revokeObjectURL(url);
}; };
const loadSample = () => { const loadSample = () => {
const sampleData = { const sampleData = {
"users": [ users: [
{"name": "Alice", "age": 30, "city": "New York"}, { name: "Alice", age: 30, city: "New York" },
{"name": "Bob", "age": 25, "city": "San Francisco"}, { name: "Bob", age: 25, city: "San Francisco" },
{"name": "Charlie", "age": 35, "city": "Chicago"} { name: "Charlie", age: 35, city: "Chicago" },
], ],
"total": 3 total: 3,
}; };
setJsonData(JSON.stringify(sampleData, null, 2)); setJsonData(JSON.stringify(sampleData, null, 2));
setJmespathExpression('users[?age > `30`].name'); setJmespathExpression("users[?age > `30`].name");
}; };
const loadFromDisk = () => { const loadFromDisk = () => {
const input = document.createElement('input'); const input = document.createElement("input");
input.type = 'file'; input.type = "file";
input.accept = '.json'; input.accept = ".json";
input.onchange = (e) => { input.onchange = (e) => {
const file = e.target.files[0]; const file = e.target.files[0];
if (file) { if (file) {
@@ -122,7 +125,7 @@ function MainPage({ apiKey, showReloadButton, onReloadSampleData, initialSampleD
const parsed = JSON.parse(content); const parsed = JSON.parse(content);
setJsonData(JSON.stringify(parsed, null, 2)); setJsonData(JSON.stringify(parsed, null, 2));
} catch (error) { } catch (error) {
alert('Invalid JSON file'); alert("Invalid JSON file");
} }
}; };
reader.readAsText(file); reader.readAsText(file);
@@ -132,9 +135,9 @@ function MainPage({ apiKey, showReloadButton, onReloadSampleData, initialSampleD
}; };
const loadLogFile = () => { const loadLogFile = () => {
const input = document.createElement('input'); const input = document.createElement("input");
input.type = 'file'; input.type = "file";
input.accept = '.log,.jsonl,.ndjson'; input.accept = ".log,.jsonl,.ndjson";
input.onchange = (e) => { input.onchange = (e) => {
const file = e.target.files[0]; const file = e.target.files[0];
if (file) { if (file) {
@@ -142,12 +145,12 @@ function MainPage({ apiKey, showReloadButton, onReloadSampleData, initialSampleD
reader.onload = (e) => { reader.onload = (e) => {
try { try {
const content = e.target.result; const content = e.target.result;
const lines = content.trim().split('\n'); const lines = content.trim().split("\n");
const logs = lines.map(line => JSON.parse(line)); const logs = lines.map((line) => JSON.parse(line));
setJsonData(JSON.stringify(logs, null, 2)); setJsonData(JSON.stringify(logs, null, 2));
setJmespathExpression('[*].message'); setJmespathExpression("[*].message");
} catch (error) { } catch (error) {
alert('Invalid JSON Lines file'); alert("Invalid JSON Lines file");
} }
}; };
reader.readAsText(file); reader.readAsText(file);
@@ -162,8 +165,9 @@ function MainPage({ apiKey, showReloadButton, onReloadSampleData, initialSampleD
<div className="row mb-2"> <div className="row mb-2">
<div className="col-12"> <div className="col-12">
<p className="text-muted text-center mb-2 small"> <p className="text-muted text-center mb-2 small">
Validate and test JMESPath expressions against JSON data in real-time. Validate and test JMESPath expressions against JSON data in
Enter your JMESPath query and JSON data below to see the results instantly. real-time. Enter your JMESPath query and JSON data below to see the
results instantly.
</p> </p>
</div> </div>
</div> </div>
@@ -172,11 +176,54 @@ function MainPage({ apiKey, showReloadButton, onReloadSampleData, initialSampleD
<div className="row mb-2"> <div className="row mb-2">
<div className="col-12"> <div className="col-12">
<div className="card"> <div className="card">
<div className="card-header d-flex justify-content-between align-items-center py-2"> <div className="card-header py-2">
<h6 className="mb-0"> <h6 className="mb-0">
<i className="bi bi-search me-2"></i> <i className="bi bi-search me-2"></i>
JMESPath Expression JMESPath Expression
</h6> </h6>
</div>
<div className="card-body">
<input
type="text"
className={`form-control jmespath-input ${error ? "error" : "success"}`}
value={jmespathExpression}
onChange={handleJmespathChange}
placeholder="Enter JMESPath expression (e.g., people[*].name)"
/>
<div
className={`alert mt-2 mb-0 d-flex justify-content-between align-items-center ${error ? "alert-danger" : "alert-success"}`}
>
<small className="mb-0">
{error || "Expression is correct"}
</small>
{showReloadButton && (
<button
className="btn btn-light btn-sm ms-2 border"
onClick={() => {
onReloadSampleData();
}}
title="New sample data is available"
>
<i className="bi bi-arrow-clockwise me-1"></i>
Reload Sample Data
</button>
)}
</div>
</div>
</div>
</div>
</div>
{/* Lower Middle Section: Input and Output Areas */}
<div className="row flex-grow-1" style={{ minHeight: 0 }}>
{/* Left Panel: JSON Data Input */}
<div className="col-md-6">
<div className="card h-100 d-flex flex-column">
<div className="card-header d-flex justify-content-between align-items-center py-2">
<h6 className="mb-0">
<i className="bi bi-file-earmark-code me-2"></i>
JSON Data
</h6>
<div> <div>
<button <button
className="btn btn-outline-success btn-sm me-2" className="btn btn-outline-success btn-sm me-2"
@@ -215,52 +262,16 @@ function MainPage({ apiKey, showReloadButton, onReloadSampleData, initialSampleD
</button> </button>
</div> </div>
</div> </div>
<div className="card-body"> <div
<input className="card-body flex-grow-1 d-flex flex-column"
type="text" style={{ minHeight: 0 }}
className={`form-control jmespath-input ${error ? 'error' : 'success'}`}
value={jmespathExpression}
onChange={handleJmespathChange}
placeholder="Enter JMESPath expression (e.g., people[*].name)"
/>
<div className={`alert mt-2 mb-0 d-flex justify-content-between align-items-center ${error ? 'alert-danger' : 'alert-success'}`}>
<small className="mb-0">{error || 'Expression is correct'}</small>
{showReloadButton && (
<button
className="btn btn-light btn-sm ms-2 border"
onClick={() => {
onReloadSampleData();
}}
title="New sample data is available"
> >
<i className="bi bi-arrow-clockwise me-1"></i>
Reload Sample Data
</button>
)}
</div>
</div>
</div>
</div>
</div>
{/* Lower Middle Section: Input and Output Areas */}
<div className="row flex-grow-1" style={{ minHeight: 0 }}>
{/* Left Panel: JSON Data Input */}
<div className="col-md-6">
<div className="card h-100 d-flex flex-column">
<div className="card-header py-2">
<h6 className="mb-0">
<i className="bi bi-file-earmark-code me-2"></i>
JSON Data
</h6>
</div>
<div className="card-body flex-grow-1 d-flex flex-column" style={{ minHeight: 0 }}>
<textarea <textarea
className={`form-control json-input flex-grow-1 ${jsonError ? 'error' : 'success'}`} className={`form-control json-input flex-grow-1 ${jsonError ? "error" : "success"}`}
value={jsonData} value={jsonData}
onChange={handleJsonChange} onChange={handleJsonChange}
placeholder="Enter JSON data here..." placeholder="Enter JSON data here..."
style={{ minHeight: 0, resize: 'none' }} style={{ minHeight: 0, resize: "none" }}
/> />
{jsonError && ( {jsonError && (
<div className="alert alert-danger mt-2 mb-0"> <div className="alert alert-danger mt-2 mb-0">
@@ -274,19 +285,42 @@ function MainPage({ apiKey, showReloadButton, onReloadSampleData, initialSampleD
{/* Right Panel: Results */} {/* Right Panel: Results */}
<div className="col-md-6"> <div className="col-md-6">
<div className="card h-100 d-flex flex-column"> <div className="card h-100 d-flex flex-column">
<div className="card-header py-2"> <div className="card-header py-2 d-flex justify-content-between align-items-center">
<h6 className="mb-0"> <h6 className="mb-0">
<i className="bi bi-output me-2"></i> <i className="bi bi-output me-2"></i>
Results Results
</h6> </h6>
<div>
<button
className={`btn btn-sm me-2 ${copySuccess ? "btn-success" : "btn-outline-secondary"}`}
onClick={copyToClipboard}
disabled={!result || result === "null"}
title="Copy result to clipboard"
>
<i className={`bi ${copySuccess ? "bi-check-lg" : "bi-clipboard"} me-1`}></i>
{copySuccess ? "Copied!" : "Copy"}
</button>
<button
className="btn btn-outline-secondary btn-sm"
onClick={downloadResult}
disabled={!result || result === "null"}
title="Download result as JSON file"
>
<i className="bi bi-download me-1"></i>
Download
</button>
</div> </div>
<div className="card-body flex-grow-1 d-flex flex-column" style={{ minHeight: 0 }}> </div>
<div
className="card-body flex-grow-1 d-flex flex-column"
style={{ minHeight: 0 }}
>
<textarea <textarea
className="form-control result-output flex-grow-1" className="form-control result-output flex-grow-1"
value={result} value={result}
readOnly readOnly
placeholder="Results will appear here..." placeholder="Results will appear here..."
style={{ minHeight: 0, resize: 'none' }} style={{ minHeight: 0, resize: "none" }}
/> />
</div> </div>
</div> </div>

View File

@@ -8,12 +8,6 @@ code {
monospace; monospace;
} }
.container-fluid {
height: 100vh;
display: flex;
flex-direction: column;
}
.content-section { .content-section {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
@@ -52,13 +46,6 @@ code {
color: var(--success-text-light); color: var(--success-text-light);
} }
.header-section {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 2rem 0;
margin-bottom: 2rem;
}
/* Dark mode support for error states */ /* Dark mode support for error states */
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
.error { .error {

View File

@@ -1,46 +0,0 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
// Add TextEncoder/TextDecoder for Node.js compatibility
if (typeof TextEncoder === 'undefined') {
global.TextEncoder = require('util').TextEncoder;
}
if (typeof TextDecoder === 'undefined') {
global.TextDecoder = require('util').TextDecoder;
}
// Mock crypto.getRandomValues for test environment
if (typeof global.crypto === 'undefined') {
global.crypto = {
getRandomValues: (array) => {
// Simple predictable mock for testing
for (let i = 0; i < array.length; i++) {
array[i] = Math.floor(Math.random() * 256);
}
return array;
}
};
}
// Suppress console errors during tests
const originalError = console.error;
beforeAll(() => {
console.error = (...args) => {
if (
typeof args[0] === 'string' &&
(args[0].includes('Warning: ReactDOMTestUtils.act is deprecated') ||
args[0].includes('Warning: An update to App inside a test was not wrapped in act'))
) {
return;
}
originalError.call(console, ...args);
};
});
afterAll(() => {
console.error = originalError;
});

25
vite.config.js Normal file
View File

@@ -0,0 +1,25 @@
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
server: {
host: '0.0.0.0',
port: 5173,
strictPort: true,
proxy: {
'/api': {
target: 'http://127.0.0.1:3000',
changeOrigin: true,
},
},
},
build: {
outDir: 'build',
},
test: {
globals: true,
environment: 'jsdom',
css: true,
},
});