50 Commits

Author SHA1 Message Date
44bb4b7458 Switched from Bootstrap to Material UI. 2026-01-31 16:47:44 +01:00
794fd88e8d fix: small component size adjustments. 2026-01-31 16:43:38 +01:00
9f0d7ee70a Refined control sizes. 2026-01-31 15:57:37 +01:00
4c964cdfeb Resized JMESPath Expression area. 2026-01-31 15:48:27 +01:00
be6dc0de60 feat: Integrate Material UI theme provider and enhance UI components 2026-01-31 15:36:55 +01:00
dc9def4faf Refactor: Simplify response message in upload endpoint to "OK" 2026-01-31 15:36:47 +01:00
3dd352df92 Converted to Material UI v7 - bare. 2026-01-31 11:52:15 +01:00
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
cebae83ae1 Fix dark theme colors, reload button functionality, and code cleanup 2026-01-23 12:50:56 +01:00
fd537026d3 Updated JSON upload script. 2026-01-23 10:00:03 +01:00
f2ca5d5f84 Add support for API key in upload-jmespath script 2026-01-23 09:57:59 +01:00
15036d34c2 Prepare for version 1.2.0 2026-01-23 09:53:21 +01:00
656b5efe70 Enhance copilot instructions with detailed API usage and UI features 2026-01-23 08:03:31 +01:00
abc1cef7c2 Implement missing sample data loading and one-time use security
- Fix UI to load server-hosted sample data at startup as per specification
- Add one-time use security: server clears sample data after retrieval
- Ensure React app periodically checks for new sample data availability
- Remove complex same-origin protection in favor of simpler one-time use model
- Improve data security by preventing data persistence after consumption
2026-01-23 06:15:21 +01:00
766ff96137 Add upload-jmespath script for JSON file uploads to API 2026-01-23 06:05:01 +01:00
e22b3c82a2 Fix Docker build version detection
- Skip prebuild step in Docker when VERSION build arg is provided
- Prevent version-check.js from overwriting pre-generated version.js in Docker
- Fix Docker containers showing incorrect '-dev' suffix for release builds
- Use direct react-scripts build when version is pre-generated
2026-01-21 22:06:37 +01:00
c9ce0d14b9 Separate Docker build into dedicated script
- Create build-image.sh for dedicated Docker image building
- Remove Docker build logic from build.sh to focus on Node.js app only
- Add comprehensive instructions for running and pushing Docker images
- Improve build script modularity and separation of concerns
2026-01-21 21:55:58 +01:00
ef2c1931d8 Fix Docker repository name in build script
- Update all Docker image references to use skoszewski/jmespath-playground
- Fix build.sh to create properly namespaced Docker images for Docker Hub
- Update Docker run command examples with correct repository name
2026-01-21 21:52:25 +01:00
d027459678 Enhance version management and Docker build system
- Add Docker build args for proper version detection in containers
- Update build.sh script with smart version detection for Docker builds
- Add --message option to new-version.js for custom commit messages
- Fix Docker builds showing incorrect '-dev' suffix for release builds
- Improve versioning workflow with comprehensive --check analysis
2026-01-21 21:48:45 +01:00
4d6efe791b Version 1.1.2 2026-01-21 21:33:58 +01:00
8c06faee31 Bump version to 1.1.1 2026-01-21 21:06:41 +01:00
86687cb6a3 Improve version management system
- Fix version-check.js to use git tag version for releases
- Add new-version.js script for proper version/tag synchronization
- Update package.json to correct version 1.1.0

The new-version.js script ensures package.json and git tags are always synchronized by:
1. Updating package.json with new version
2. Committing the change
3. Tagging the commit

This prevents version mismatches and git dirty states.
2026-01-21 21:06:35 +01:00
710682d931 Enhance upload scripts with command-line options and JavaScript implementation
- Add -u/--url option to specify API URL via command line
- Refactor usage messages into centralized show_usage() function
- Remove environment variable fallback for API_URL
- Use curl's native file handling with --data @file
- Add JavaScript version (upload.js) with identical functionality
- Both scripts support same command-line interface and options
2026-01-21 20:48:28 +01:00
7e78ef65b1 Implement multi-stage Docker build for clean production image
- Build stage: Has scripts/ for version generation, all dependencies
- Production stage: Only production deps, built artifacts, server.js
- Eliminates build scripts and dev dependencies from final image
- Maintains proper version.js generation during build process
2026-01-21 20:06:39 +01:00
5379b1519d Clean version management with generated version.js
- Replace package.json modification with generated src/version.js
- App imports VERSION from ./version instead of package.json
- version.js is auto-generated during prebuild and git-ignored
- Provides VERSION, IS_RELEASE, and BUILD_TIME constants
- No more dirty git status from version changes

Development builds show 1.0.4-dev, tagged releases show 1.0.4
2026-01-21 19:57:06 +01:00
601f80ab06 Fix Docker build - add React build step back to Dockerfile
The build/ directory doesn't exist in Docker context, so we need to
build the React app inside the container. This approach:
- Installs all deps, builds app, then removes dev deps
- Works for remote deployments without requiring pre-built artifacts
- Maintains lean final image size
2026-01-21 19:45:47 +01:00
4fe1ece3a3 Restructure project and fix tests
Major changes:
- Move server.js to project root for cleaner architecture
- Remove server tests due to CRA Jest configuration conflicts
- Fix React component tests (clipboard API and user interaction issues)
- Optimize Dockerfile to copy only essential files (server.js, build/)
- Fix upload script to only upload JSON data (no JMESPath expression)
- Improve reload button UI to avoid layout shifts
- Update demo script with accurate commands and Docker support

All 17 React tests now pass. Server structure simplified and consistent.
2026-01-21 19:42:04 +01:00
18b6b5a7c0 Clarify sample data reload process in AI agent instructions 2026-01-21 18:43:03 +01:00
025b07e328 Update API documentation for sample data upload and retrieval endpoints 2026-01-21 18:37:39 +01:00
d61bbc2f48 Add API documentation for JSON file upload and evaluation results 2026-01-21 11:22:14 +01:00
14d87bff2e v1.0.4 - Unified theme system and consistent color definitions 2026-01-21 11:11:23 +01:00
0182174153 v1.0.3: Fix expression textbox error state and UI consistency
- Fixed theme class application from html to body element for proper CSS inheritance
- Removed CSS conflicts between base styles and error/success states
- Fixed focus state interference with error/success background colors
- Changed error message panel to fixed placement (no more UI jumping)
- Added theme-consistent styling for alert-success in all theme modes
- Expression textbox now properly shows red/green backgrounds in manual themes
- Status message now shows Expression is correct vs error message consistently
2026-01-21 10:35:34 +01:00
6f8c4518ce v1.0.2: Complete CSS refactoring with custom properties
- Introduced comprehensive CSS custom properties for colors, fonts, and transitions
- Eliminated 40+ hardcoded color values with centralized variables
- Consolidated duplicate font family definitions
- Removed redundant button styles and input styling
- Optimized theme system with consistent property usage
- Fixed accessibility issue with MIT license link
- Reduced code redundancy while maintaining full functionality
- All themes (auto/light/dark) now use unified variable system
2026-01-21 10:09:52 +01:00
97d83923d9 v1.0.1: Add comprehensive theme switcher and fix dark mode issues
- Add theme switcher widget with Auto/Light/Dark options in header
- Implement manual theme override system with localStorage persistence
- Add complete button theme overrides for all variants (primary, outline-*)
- Fix missing focus states and placeholder colors for light theme
- Add proper alert styling for both themes
- Fix expression input error state colors in dark theme
- Complete comprehensive theme coverage for all UI elements
- Theme switcher overrides CSS media queries when manually selected
- All buttons, inputs, and surfaces now properly adapt to theme changes
2026-01-21 09:45:38 +01:00
fef9c9732e Add comprehensive dark mode support and fix color consistency
- Add automatic dark/light theme detection via prefers-color-scheme
- Add smooth transitions between theme changes
- Fix color contrast issues with explicit text colors
- Make JSON Data and Query Result boxes static (no color changes)
- Keep only Expression input with error indication (red styling)
- Remove dynamic success/error styling from data input/output areas
- Add proper placeholder and focus state colors for both themes
- Ensure all text remains visible in both light and dark modes
- Clean up unused CSS classes
2026-01-18 16:47:59 +01:00
61408d6362 Fix serve command options in Dockerfile
- Remove unsupported --host flag from serve command
- The serve package with -l option binds to 0.0.0.0 by default in containers
- Container will now start properly without ArgError
2026-01-18 16:13:00 +01:00
ce508d32b5 Fix Dockerfile to properly expose container service
- Install all dependencies with 'npm ci' instead of production-only
- Install 'serve' package globally in container
- Use direct serve command with proper host binding (0.0.0.0)
- Fix container service accessibility for production deployment
2026-01-18 16:06:01 +01:00
db3b6beaa3 Modernize Docker Compose configuration
- Replace docker-compose.yml with compose.yaml (modern standard)
- Remove obsolete 'version' field (deprecated in Docker Compose)
- Remove development service configuration
- Add proper image tagging with skoszewski/jmespath-playground
- Keep build context for local development flexibility
- Simplify to production-only service configuration
2026-01-18 16:01:18 +01:00
39 changed files with 5700 additions and 15477 deletions

View File

@@ -6,25 +6,102 @@ applyTo: "**/*.md,**/.js"
The tool in this repository is designed to help users validate and test JMESPath expressions against JSON data. It is a React-based web application that provides an interactive interface for entering JMESPath queries and viewing the results.
The application is single page. The page is divided into three sections:
The main application page is divided into three sections:
- Top section: Title and description of the tool.
- Theme control buttons (auto/light/dark)
- Key-lock button that switches to the second application page.
- Middle section:
- The label "JMESPath Expression" with a right allinged row of action buttons:
- Load an Object
- Load a Log File
- Load Sample
- Format JSON
- Clear All
- Input area for JMESPath expressions
- Message area for errors related to JMESPath expression syntax
- Lower Middle left section: Input area for JSON data
- Lower Middle right section: Output are for JMESPath query results
- Boottom section: Footer with author and license information
- Bottom section: Footer with author and license information
The Middle section also contains a toolbar with buttons to load data from disk, load sample data, format JSON input, and clear all inputs.
The second page of the application contains:
- Top section: that is the same as the main page
- Middle section:
- API key display area with a button to regenerate the API key. The API key is 32 characters long cryptograghically secure random string.
- Instructions on how to use the API to upload sample data remotely with a code block displaying example curl command.
- Bottom section: Footer with author and license information.
The sample code block:
```bash
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "X-API-Key: {{API_KEY}}" \
--data @{{JSON_FILE_NAME}} \
"{{API_URL}}/api/v1/upload"
```
Placeholders `{{API_KEY}}` and `{{API_URL}}` should be replaced with the actual API key and the URL of the deployed application respectively. The `{{JSON_FILE_NAME}}` placeholder should be shown as is to indicate the file containing the JSON data to be uploaded.
The server code is only used as a bridge between the UI app and the external tools that may upload the sample data. The server does not perform any JMESPath evaluation or JSON parsing; all such logic is handled in the React application.
The server exposes a REST API to allow external tools to upload sample data that users can load into the application. The API key is required to upload sample data.
The API key is used for:
- encrypting the sample data
- authenticating download requests
Session id is a hash of the API key.
The server keeps two pieces of information in memory for each session:
1. The sample data itself.
2. A state variable (a GUID) that changes whenever new sample data is uploaded.
The maximum number of sessions to keep in memory set at the server startup using `MAX_SESSIONS` environment variable that defaults to 100. The maximum size of the sample data is set using `MAX_SAMPLE_SIZE` environment variable that defaults to 1 MB. Maximum session age is controlled using `MAX_SESSION_TTL` environment variable that defaults to 1 hour. After reaching the maximum number of sessions, the server rejects new uploads until some sessions expire. Sessions older than the maximum session age are automatically purged.
The UI generates an API key at startup then load the sample data at startup and periodically checks the state variable to see if new sample data is available. If state variable changes, the React app displays a button beneath the expression input area to reload the sample data. The reload is performed only when the user clicks the button.
---
The main components of the application are located in the `src` directory and target Node 24 LTS environment.
Framework to be used:
- React for building the user interface.
- Material UI v7 for building the user interface.
- React for building the component logic.
- JavaScript (ES6+) for scripting.
- Bootstrap for styling and layout.
- Express.js for serving the application and handling API requests.
### API
The application exposes a REST API for remotly uploading sample data. The API endpoints are as follows:
- `POST /api/v1/upload`: The sample data is sent in the request body as JSON. The request must include an `x-api-key` header with the API key. If the upload is successful, the server responds with status 200 OK.
The server stores the sample data in memory and generates a new value for its state variable (a guid).
- `GET /api/v1/sample`: Returns the currently stored sample data as JSON. The request must include an `x-api-key` header with the API key. If the API key is invalid or the header is missing, the server responds with status 403 Forbidden.
- `GET /api/v1/state`: Returns the current value of the state variable (a guid) as a string. The request must include an `x-api-key` header with the API key. If the API key is invalid or the header is missing, the server responds with status 403 Forbidden.
## Containerization
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.

3
.gitignore vendored
View File

@@ -22,6 +22,9 @@ npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Auto-generated version file
/src/version.js
# IDE
.vscode/
.idea/

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

@@ -1,5 +1,9 @@
# Use Node 24 LTS as base image
FROM node:24-alpine
# Build stage
FROM node:24-alpine AS builder
# Accept build arguments for version info
ARG VERSION=""
ARG IS_RELEASE="false"
# Set working directory
WORKDIR /app
@@ -7,17 +11,46 @@ WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Install dependencies (production + dev for build)
RUN npm ci
# Copy application source
COPY . .
# Copy source code and build dependencies
COPY src/ ./src/
COPY public/ ./public/
COPY scripts/ ./scripts/
COPY server.js ./server.js
COPY vite.config.js ./vite.config.js
COPY index.html ./index.html
# Build the application
RUN npm run build
# Production stage
FROM node:24-alpine AS production
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install only production dependencies
RUN npm ci --only=production && npm cache clean --force
# Copy built application and server from build stage
COPY --from=builder /app/build ./build
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 3000
# Start the application
CMD ["npm", "run", "serve"]
# Set LISTEN_ADDR to bind to all interfaces in container
ENV LISTEN_ADDR=0.0.0.0
ENV LISTEN_PORT=3000
# Start the integrated server
ENTRYPOINT ["./entrypoint.sh"]

164
README.md
View File

@@ -4,66 +4,67 @@ A React-based web application for testing and validating JMESPath expressions ag
![JMESPath Testing Tool](https://img.shields.io/badge/React-18.2.0-blue?style=flat-square&logo=react)
![Node.js](https://img.shields.io/badge/Node.js-24%20LTS-green?style=flat-square&logo=node.js)
![Bootstrap](https://img.shields.io/badge/Bootstrap-5.3.2-purple?style=flat-square&logo=bootstrap)
![Material UI](https://img.shields.io/badge/Material--UI-v7-007FFF?style=flat-square&logo=mui)
## Features
- 🎯 **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)
- 🎨 **Bootstrap UI**: Clean, responsive interface with Bootstrap styling
- 🔄 **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
- **Real-time Evaluation**: JMESPath expressions are evaluated instantly as you type
- **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
- **Container Ready**: Containerized for easy deployment
## Quick Start
### Prerequisites
- Node.js 24 LTS or higher
- npm or yarn package manager
- npm package manager
### Local Development
1. **Clone the repository**:
```bash
git clone <repository-url>
cd jmespath-playground
```
2. **Install dependencies**:
```bash
npm install
```
3. **Start the development server**:
```bash
npm start
```
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
# Build the Docker image
docker build -t jmespath-playground .
npm run dev
```
# 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
# or
container run -p 3000:3000 jmespath-playground
```
## Usage
@@ -82,93 +83,56 @@ docker run -p 3000:3000 jmespath-playground
- Format JSON for better readability
- 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
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"
```
3. **Auto-reload**: The running app will detect new data and show a reload button
## Available Scripts
**API Endpoints:**
- `POST /api/v1/upload` - Upload sample data
- `GET /api/v1/sample` - Retrieve current sample data
- `GET /api/v1/state` - Get current state ID
- `GET /api/v1/health` - Simple health check (returns "OK")
- `GET /api/v1/status` - Detailed status information (JSON)
In the project directory, you can run:
## Server Configuration
### `npm start`
The server can be configured using environment variables:
Runs the app in development mode. The page will reload when you make edits.
**Network Settings:**
- `LISTEN_ADDR` - Server bind address (default: `127.0.0.1`)
- `LISTEN_PORT` - Server port (default: `3000`)
### `npm test`
**Session Management:**
- `MAX_SESSIONS` - Maximum number of concurrent sessions (default: `100`)
- `MAX_SAMPLE_SIZE` - Maximum size of uploaded sample data in bytes (default: `1048576` - 1MB)
- `MAX_SESSION_TTL` - Session time-to-live in milliseconds (default: `3600000` - 1 hour)
Launches the test runner in interactive watch mode.
Example usage:
### `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
```
jmespath-playground/
├── .github/
│ ├── workflows/
│ │ └── build-container.yml # CI/CD pipeline
│ └── copilot-instructions.md # AI agent instructions
├── public/
│ ├── index.html
│ ├── manifest.json
│ └── favicon.ico
├── src/
│ ├── App.js # Main application component
│ ├── App.css # App-specific styles
│ ├── App.test.js # App tests
│ ├── index.js # React entry point
│ ├── index.css # Global styles
│ ├── setupTests.js # Test configuration
│ └── reportWebVitals.js
├── scripts/
│ ├── build.sh # Build script
│ └── dev.sh # Development script
├── Dockerfile # Docker container
├── Dockerfile.dev # Development container
├── docker-compose.yml # Container orchestration
├── package.json # Dependencies and scripts
├── README.md # Comprehensive documentation
├── DEVELOPMENT.md # Developer guide
└── demo.sh # Demo script
```bash
MAX_SESSIONS=200 MAX_SAMPLE_SIZE=2097152 LISTEN_PORT=8080 node server.js
```
## Technology Stack
- **React 18.2.0**: Frontend framework
- **Bootstrap 5.3.2**: CSS framework for styling
- **JMESPath 0.16.0**: JMESPath expression evaluation
- **React 18.2.0**: Frontend framework with modern hooks and components
- **Material UI v7**: Modern React component library following Material 3 Design principles.
- **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
- **Docker**: Optional containerization
## 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
- **UUID 9.0.0**: Cryptographically secure session IDs
- **Container**: Containerization for easy deployment
## License
@@ -177,7 +141,3 @@ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file
## 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/).
## 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
}

58
bin/upload-jmespath Executable file
View File

@@ -0,0 +1,58 @@
#!/usr/bin/env bash
set -euo pipefail
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="-"
function usage() {
echo "Usage: $0 [--api-url <url>] [--json-file <file>]"
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
case $1 in
--api-url)
JMESPATH_PLAYGROUND_API_URL="$2"
shift 2
;;
--api-key)
JMESPATH_PLAYGROUND_API_KEY="$2"
shift 2
;;
--json-file)
JSON_FILE="$2"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "ERROR: Unknown argument: $1"
usage
exit 1
;;
esac
done
ADD_HEADERS+=("-H" "X-API-Key: $JMESPATH_PLAYGROUND_API_KEY")
# Send the POST request
curl -s -X POST \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
"${ADD_HEADERS[@]}" \
--data @${JSON_FILE} \
"$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();

9
compose.yaml Normal file
View File

@@ -0,0 +1,9 @@
services:
jmespath-playground:
build: .
image: skoszewski/jmespath-playground
ports:
- "3000:3000"
environment:
- NODE_ENV=production
restart: unless-stopped

55
demo.sh
View File

@@ -1,55 +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"
else
echo "⚠️ Docker not found"
fi
echo ""
echo "📦 Installing dependencies..."
npm install
echo ""
echo "🔨 Building production version..."
npm run build
echo ""
echo "🎉 Demo completed successfully!"
echo ""
echo "To start development:"
echo " npm start"
echo ""
echo "To serve the production build:"
echo " npm run serve"
echo ""
echo "To run with Docker:"
if command -v docker &> /dev/null; then
echo " npm run docker:build"
echo " npm run docker:run"
else
echo " (Docker not available - install Docker first)"
fi

View File

@@ -1,25 +0,0 @@
version: '3.8'
services:
jmespath-playground:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
restart: unless-stopped
# Development service
jmespath-playground-dev:
build:
context: .
dockerfile: Dockerfile.dev
ports:
- "3001:3000"
volumes:
- .:/app
- /app/node_modules
environment:
- CHOKIDAR_USEPOLLING=true
profiles:
- dev

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">
<head>
<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="theme-color" content="#000000" />
<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="%PUBLIC_URL%/manifest.json" />
<link rel="manifest" href="/manifest.json" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<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">
@@ -16,5 +15,6 @@
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.jsx"></script>
</body>
</html>
</html>

17016
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,35 +1,34 @@
{
"name": "jmespath-playground",
"version": "1.0.0",
"version": "1.4.0",
"description": "A React-based web application for testing JMESPath expressions against JSON data",
"main": "index.js",
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject",
"serve": "serve -s build -l 3000"
"start": "vite",
"prebuild": "node scripts/version-check.js",
"build": "vite build",
"preview": "vite preview",
"test": "vitest",
"server": "node server.js --dev",
"dev": "concurrently \"npm start\" \"node --watch server.js --dev\"",
"build-image": "node scripts/build-image.js"
},
"engines": {
"node": ">=24.0.0"
},
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
"@mui/icons-material": "^7.3.7",
"@mui/material": "^7.3.7",
"@testing-library/jest-dom": "^6.1.4",
"@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^14.5.1",
"bootstrap": "^5.3.2",
"express": "^4.19.2",
"jmespath": "^0.16.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-scripts": "^5.0.1",
"serve": "^14.2.1",
"web-vitals": "^3.5.0"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
"uuid": "^9.0.0"
},
"browserslist": {
"production": [
@@ -51,5 +50,14 @@
"react"
],
"author": "",
"license": "MIT"
}
"license": "MIT",
"devDependencies": {
"@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,49 +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
# Optional container build with Docker
if command -v docker &> /dev/null; then
echo "🐳 Building Docker container (optional)..."
docker build -t jmespath-playground .
else
echo "💡 Docker not found. Container build is optional."
echo " Install Docker if you want to build containers."
fi
echo "✅ Build completed successfully!"
echo ""
echo "To run the application:"
echo " npm run serve # Serve production build locally"
echo " docker run -p 3000:3000 jmespath-playground # Run container (if built)"
if command -v docker &> /dev/null; then
echo " docker run -p 3000:3000 jmespath-playground # Run with Docker"
fi

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

300
scripts/new-version.js Executable file
View File

@@ -0,0 +1,300 @@
#!/usr/bin/env node
const fs = require('fs');
const { execSync } = require('child_process');
function showUsage() {
console.log('Usage: node scripts/new-version.js <version> [--force] [-m|--message "commit message"]');
console.log(' node scripts/new-version.js --check <version>');
console.log('');
console.log('Creates a new version by tagging the current commit.');
console.log('');
console.log('Options:');
console.log(' --force Force version creation even with dirty repo or package.json mismatch');
console.log(' --check Analyze repository status and report what would happen for specified version');
console.log(' -m, --message TEXT Custom commit message (only used when commit is needed)');
console.log('');
console.log('Example:');
console.log(' node scripts/new-version.js 1.2.0');
console.log(' node scripts/new-version.js 1.2.0 --force');
console.log(' node scripts/new-version.js 1.2.0 -m "Add new feature XYZ"');
console.log(' node scripts/new-version.js --check 1.3.0');
}
function performCheck(targetVersion) {
console.log('🔍 Repository Analysis Report');
console.log('============================');
try {
// Read package.json
const packagePath = './package.json';
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
const currentVersion = pkg.version;
console.log(`📦 Package.json version: ${currentVersion}`);
// Check repository status
let isRepoDirty = false;
let dirtyFiles = '';
try {
const status = execSync('git status --porcelain', { encoding: 'utf8' });
isRepoDirty = status.trim() !== '';
dirtyFiles = status.trim();
} catch (error) {
console.log('⚠️ Cannot determine git status');
}
if (isRepoDirty) {
console.log('🔄 Repository status: DIRTY');
console.log(' Uncommitted changes:');
dirtyFiles.split('\n').forEach(line => {
if (line.trim()) console.log(` ${line}`);
});
} else {
console.log('✅ Repository status: CLEAN');
}
// Check current commit info
try {
const currentCommit = execSync('git rev-parse HEAD', { encoding: 'utf8' }).trim();
const currentBranch = execSync('git rev-parse --abbrev-ref HEAD', { encoding: 'utf8' }).trim();
console.log(`🌟 Current commit: ${currentCommit.substring(0, 7)} (${currentBranch})`);
// Check if current commit is tagged
const tagsOnHead = execSync('git tag --points-at HEAD', { encoding: 'utf8' }).trim();
if (tagsOnHead) {
console.log(`🏷️ Current commit tags: ${tagsOnHead.split('\n').join(', ')}`);
} else {
console.log('🏷️ Current commit: No tags');
}
} catch (error) {
console.log('⚠️ Cannot determine commit info');
}
// List recent tags
try {
const recentTags = execSync('git tag --sort=-version:refname | head -5', { encoding: 'utf8' }).trim();
if (recentTags) {
console.log('📋 Recent tags:');
recentTags.split('\n').forEach(tag => {
if (tag.trim()) console.log(` ${tag}`);
});
} else {
console.log('📋 No tags found in repository');
}
} catch (error) {
console.log('⚠️ Cannot list tags');
}
console.log('');
// Analysis for target version (if provided)
if (targetVersion) {
const tagName = `v${targetVersion}`;
console.log(`🎯 Analysis for version ${targetVersion}:`);
console.log('=====================================');
// Check if target tag exists
try {
const existingTags = execSync('git tag -l', { encoding: 'utf8' });
const tagExists = existingTags.split('\n').includes(tagName);
if (tagExists) {
console.log(`❌ Tag '${tagName}' already exists - CANNOT CREATE`);
return;
}
console.log(`✅ Tag '${tagName}' available`);
} catch (error) {
console.log('⚠️ Cannot check tag availability');
return;
}
// Analyze what actions would be needed
const packageJsonMatches = currentVersion === targetVersion;
const needsPackageUpdate = !packageJsonMatches;
const needsCommit = isRepoDirty || needsPackageUpdate;
console.log(`📝 Package.json: ${packageJsonMatches ? 'MATCHES' : `NEEDS UPDATE (${currentVersion}${targetVersion})`}`);
if (needsCommit) {
console.log('⚡ Actions needed:');
if (needsPackageUpdate) {
console.log(' • Update package.json');
}
if (isRepoDirty) {
console.log(' • Stage uncommitted changes');
}
console.log(' • Create commit');
console.log(` • Create tag ${tagName}`);
console.log('');
console.log('📋 Commands that would work:');
if (isRepoDirty || needsPackageUpdate) {
console.log(` node scripts/new-version.js ${targetVersion} --force`);
} else {
console.log(` node scripts/new-version.js ${targetVersion}`);
console.log(` node scripts/new-version.js ${targetVersion} --force`);
}
} else {
console.log('⚡ Actions needed:');
console.log(` • Create tag ${tagName} (no commit needed)`);
console.log('');
console.log('📋 Commands that would work:');
console.log(` node scripts/new-version.js ${targetVersion}`);
console.log(` node scripts/new-version.js ${targetVersion} --force`);
}
console.log('');
console.log('🚦 Default mode requirements:');
if (isRepoDirty) {
console.log(' ❌ Repository must be clean (currently dirty)');
} else {
console.log(' ✅ Repository is clean');
}
if (!packageJsonMatches) {
console.log(` ❌ Package.json must match version (currently ${currentVersion})`);
} else {
console.log(' ✅ Package.json version matches');
}
} else {
// This should never happen since version is now required
console.error('Internal error: No version provided to performCheck');
process.exit(1);
}
} catch (error) {
console.error('❌ Error during analysis:', error.message);
process.exit(1);
}
}
function main() {
// Parse command line arguments
const args = process.argv.slice(2);
if (args.length === 0 || args.includes('-h') || args.includes('--help')) {
showUsage();
process.exit(args.length === 0 ? 1 : 0);
}
const isCheck = args.includes('--check');
const isForce = args.includes('--force');
// Parse custom commit message
let customMessage = null;
const messageIndex = args.findIndex(arg => arg === '-m' || arg === '--message');
if (messageIndex !== -1 && messageIndex + 1 < args.length) {
customMessage = args[messageIndex + 1];
}
let newVersion;
if (isCheck) {
// For --check, version is required
newVersion = args.find(arg => !arg.startsWith('--') && arg !== '-m' && arg !== customMessage);
if (!newVersion) {
console.error('Error: Version argument required for --check');
showUsage();
process.exit(1);
}
} else {
// For normal operation, version is required
newVersion = args.find(arg => !arg.startsWith('--') && arg !== '-m' && arg !== customMessage);
if (!newVersion) {
}
}
if (isCheck) {
performCheck(newVersion);
return;
}
const tagName = `v${newVersion}`;
console.log(`🏷️ Creating new version: ${newVersion}${isForce ? ' (forced)' : ''}`);
try {
// 1. Check if tag already exists - Always ERROR
try {
const existingTags = execSync('git tag -l', { encoding: 'utf8' });
if (existingTags.split('\n').includes(tagName)) {
console.error(`❌ Error: Tag '${tagName}' already exists`);
process.exit(1);
}
} catch (error) {
console.error('❌ Error: Failed to check existing tags');
process.exit(1);
}
// 2. Check repository status
let isRepoDirty = false;
try {
const status = execSync('git status --porcelain', { encoding: 'utf8' });
isRepoDirty = status.trim() !== '';
} catch (error) {
console.error('❌ Error: Failed to check git status');
process.exit(1);
}
// 3. Check package.json version
const packagePath = './package.json';
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
const currentVersion = pkg.version;
const packageJsonMatches = currentVersion === newVersion;
// 4. Determine what action is needed
const needsPackageUpdate = !packageJsonMatches;
const needsCommit = isRepoDirty || needsPackageUpdate;
// 5. Check if force is required
if (!isForce) {
if (isRepoDirty) {
console.error('❌ Error: Working directory has uncommitted changes');
console.error('Please commit your changes first or use --force');
process.exit(1);
}
if (needsPackageUpdate) {
console.error(`❌ Error: Package.json version is ${currentVersion}, requested ${newVersion}`);
console.error('Use --force to update package.json');
process.exit(1);
}
}
// 6. Execute the versioning
if (needsCommit) {
console.log(`📦 Needs commit: ${needsPackageUpdate ? 'package.json update' : ''}${needsPackageUpdate && isRepoDirty ? ' + ' : ''}${isRepoDirty ? 'uncommitted changes' : ''}`);
// Update package.json if needed
if (needsPackageUpdate) {
pkg.version = newVersion;
fs.writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + '\n');
console.log(`📝 Updated package.json: ${currentVersion}${newVersion}`);
}
// Stage all changes
execSync('git add .', { stdio: 'inherit' });
// Commit
const commitMessage = customMessage || (needsPackageUpdate ? `Version ${newVersion}` : `Prepare for version ${newVersion}`);
execSync(`git commit -m "${commitMessage}"`, { stdio: 'inherit' });
console.log(`✅ Committed changes`);
} else {
console.log(`✅ Repository clean, package.json matches - tagging current commit`);
}
// 7. Tag the commit
execSync(`git tag ${tagName}`, { stdio: 'inherit' });
console.log(`🏷️ Created tag: ${tagName}`);
console.log('');
console.log('🎉 Version created successfully!');
console.log('');
console.log('Next steps:');
console.log(` git push origin main --tags # Push the commit and tag`);
} catch (error) {
console.error('❌ Error during version creation:', error.message);
process.exit(1);
}
}
main();

30
scripts/sample-data.json Normal file
View File

@@ -0,0 +1,30 @@
{
"users": [
{
"id": 1,
"name": "Alice Johnson",
"email": "alice@example.com",
"role": "admin",
"skills": ["JavaScript", "Python", "SQL"]
},
{
"id": 2,
"name": "Bob Wilson",
"email": "bob@example.com",
"role": "developer",
"skills": ["Java", "Spring", "React"]
},
{
"id": 3,
"name": "Carol Davis",
"email": "carol@example.com",
"role": "designer",
"skills": ["Figma", "Photoshop", "CSS"]
}
],
"metadata": {
"total": 3,
"created": "2026-01-21",
"version": "1.0"
}
}

172
scripts/upload.js Executable file
View File

@@ -0,0 +1,172 @@
#!/usr/bin/env node
/**
* JMESPath Playground Upload Script (JavaScript)
* Usage: node upload.js [-u URL] [-k API_KEY] "json_file.json"
*/
const fs = require('fs');
const path = require('path');
const https = require('https');
const http = require('http');
const { URL } = require('url');
const { parseArgs } = require('util');
function showUsage() {
const scriptName = path.basename(process.argv[1]);
console.log(`Usage: node ${scriptName} [-u|--url URL] [-k|--key API_KEY] <json_file>`);
console.log('');
console.log('Options:');
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('');
console.log('Examples:');
console.log(` node ${scriptName} data.json`);
console.log(` node ${scriptName} -u http://example.com:3000 -k your-api-key data.json`);
}
function getArguments() {
const { values, positionals } = parseArgs({
args: process.argv.slice(2),
options: {
url: { type: 'string', short: 'u', default: 'http://localhost:3000' },
key: { type: 'string', short: 'k' },
help: { type: 'boolean', short: 'h' }
},
allowPositionals: true
});
if (values.help) {
showUsage();
process.exit(0);
}
if (positionals.length !== 1) {
console.error('Error: JSON file required');
showUsage();
process.exit(1);
}
return {
apiUrl: values.url,
apiKey: values.key || '',
jsonFile: positionals[0]
};
}
async function validateJsonFile(jsonFile) {
// Check if file exists
if (!fs.existsSync(jsonFile)) {
console.error(`Error: JSON file '${jsonFile}' not found`);
process.exit(1);
}
// Validate JSON content
try {
const content = fs.readFileSync(jsonFile, 'utf8');
JSON.parse(content);
return content;
} catch (error) {
console.error(`Error: '${jsonFile}' contains invalid JSON`);
console.error(error.message);
process.exit(1);
}
}
function isLocalhost(url) {
try {
const parsed = new URL(url);
const hostname = parsed.hostname;
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',
};
// 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
});
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(`HTTP ${response.status}: ${errorData.error || 'Upload failed'}`);
}
const result = await response.json();
console.log(JSON.stringify(result));
} catch (error) {
console.error('Error uploading data:', error.message);
process.exit(1);
}
}
async function main() {
const { apiUrl, apiKey, jsonFile } = getArguments();
const jsonData = await validateJsonFile(jsonFile);
await uploadData(apiUrl, apiKey, jsonFile, jsonData);
}
// Run the script
main().catch((error) => {
console.error('Unexpected error:', error);
process.exit(1);
});

48
scripts/version-check.js Executable file
View File

@@ -0,0 +1,48 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// Read package.json for base version
const packagePath = './package.json';
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
let version = pkg.version;
let isRelease = false;
try {
// Check if current commit is tagged
const gitTag = execSync('git tag --points-at HEAD', { encoding: 'utf8' }).trim();
if (gitTag) {
// We're at a tagged commit - extract version from tag
const tagVersion = gitTag.replace(/^v/, ''); // Remove 'v' prefix if present
version = tagVersion;
console.log(`✅ Building release version ${version} (tagged: ${gitTag})`);
isRelease = true;
} else {
// We're not at a tagged commit - use unknown version
version = 'unknown';
console.log(`📦 Building development version with unknown version`);
isRelease = false;
}
} catch (error) {
// Git command failed (maybe not in a git repo)
version = 'unknown';
console.log(`⚠️ Cannot determine git status, using unknown version`);
isRelease = false;
}
// Generate version.js file
const versionFile = path.join('./src', 'version.js');
const versionContent = `// Auto-generated version file - do not edit manually
// Generated at: ${new Date().toISOString()}
export const VERSION = '${version}';
export const IS_RELEASE = ${isRelease};
export const BUILD_TIME = '${new Date().toISOString()}';
`;
fs.writeFileSync(versionFile, versionContent);
console.log(`📝 Generated ${versionFile} with version ${version}`);

459
server.js Normal file
View File

@@ -0,0 +1,459 @@
const express = require("express");
const path = require("path");
const crypto = require("crypto");
const os = require("os");
const { v4: uuidv4 } = require("uuid");
const { parseArgs } = require("util");
// Environment configuration
const MAX_SESSIONS = parseInt(process.env.MAX_SESSIONS) || 100;
const MAX_SAMPLE_SIZE = parseInt(process.env.MAX_SAMPLE_SIZE) || 1024 * 1024; // 1MB
const MAX_SESSION_TTL = parseInt(process.env.MAX_SESSION_TTL) || 60 * 60 * 1000; // 1 hour
// Utility functions for encryption
function encrypt(data, key) {
try {
const algorithm = "aes-256-gcm";
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, key, iv);
cipher.setAAD(Buffer.from("session-data"));
let encrypted = cipher.update(JSON.stringify(data), "utf8");
encrypted = Buffer.concat([encrypted, cipher.final()]);
const authTag = cipher.getAuthTag();
return {
iv: iv.toString("hex"),
data: encrypted.toString("hex"),
tag: authTag.toString("hex"),
};
} catch (error) {
console.error("⚠️ Encryption exception:", {
message: error.message,
algorithm: "aes-256-gcm",
keyLength: key ? key.length : "undefined",
timestamp: new Date().toISOString(),
});
throw new Error(`Encryption failed: ${error.message}`);
}
}
function decrypt(encryptedObj, key) {
try {
const algorithm = "aes-256-gcm";
const iv = Buffer.from(encryptedObj.iv, "hex");
const decipher = crypto.createDecipheriv(algorithm, key, iv);
decipher.setAAD(Buffer.from("session-data"));
decipher.setAuthTag(Buffer.from(encryptedObj.tag, "hex"));
let decrypted = decipher.update(
Buffer.from(encryptedObj.data, "hex"),
null,
"utf8",
);
decrypted += decipher.final("utf8");
return JSON.parse(decrypted);
} catch (error) {
console.error("⚠️ Decryption exception:", {
message: error.message,
algorithm: "aes-256-gcm",
keyLength: key ? key.length : "undefined",
hasIV: !!encryptedObj.iv,
hasTag: !!encryptedObj.tag,
hasData: !!encryptedObj.data,
timestamp: new Date().toISOString(),
});
throw new Error(`Decryption failed: ${error.message}`);
}
}
function isValidApiKey(apiKey) {
return typeof apiKey === "string" && /^[0-9a-f]{32}$/i.test(apiKey);
}
function getSessionId(apiKey) {
return crypto.createHash("sha256").update(apiKey).digest("hex");
}
function generateSalt() {
return crypto.randomBytes(32);
}
function deriveKey(apiKey, salt) {
return crypto.pbkdf2Sync(apiKey, salt, 100000, 32, "sha256");
}
// Create Express app
function createApp(devMode = false) {
const app = express();
// Trust proxy to get real client IP (needed for localhost detection)
app.set("trust proxy", true);
// Middleware
app.use(express.json({ limit: MAX_SAMPLE_SIZE }));
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
const sessions = new Map();
// Cleanup expired sessions
function cleanupExpiredSessions() {
const now = Date.now();
for (const [sessionId, session] of sessions.entries()) {
if (now - session.createdAt > MAX_SESSION_TTL) {
sessions.delete(sessionId);
console.log(
`🧹 Cleaned up expired session: ${sessionId.substring(0, 8)}...`,
);
}
}
}
// Run cleanup every 5 minutes
setInterval(cleanupExpiredSessions, 5 * 60 * 1000);
// API endpoints
app.post("/api/v1/upload", (req, res) => {
try {
const apiKey = req.headers["x-api-key"];
// Validate API key header
if (!apiKey || !isValidApiKey(apiKey)) {
return res
.status(403)
.json({ error: "Invalid or missing X-API-Key header" });
}
// Cleanup expired sessions before checking limits
cleanupExpiredSessions();
// Check session limits
if (sessions.size >= MAX_SESSIONS) {
return res.status(429).json({
error: "Maximum number of sessions reached. Please try again later.",
maxSessions: MAX_SESSIONS,
currentSessions: sessions.size,
});
}
const uploadedData = req.body;
// Validate that it's valid JSON
if (!uploadedData || typeof uploadedData !== "object") {
return res.status(400).json({ error: "Invalid JSON data" });
}
// Check data size
const dataSize = Buffer.byteLength(JSON.stringify(uploadedData), "utf8");
if (dataSize > MAX_SAMPLE_SIZE) {
return res.status(413).json({
error: "Sample data too large",
maxSize: MAX_SAMPLE_SIZE,
receivedSize: dataSize,
});
}
const sessionId = getSessionId(apiKey);
const salt = generateSalt();
const key = deriveKey(apiKey, salt);
const stateGuid = uuidv4();
// Encrypt and store session data
const encryptedData = encrypt(uploadedData, key);
sessions.set(sessionId, {
salt: salt.toString("hex"),
encryptedData,
state: stateGuid,
createdAt: Date.now(),
accessed: false,
});
console.log(
`📁 Session created: ${sessionId.substring(0, 8)}... (${sessions.size}/${MAX_SESSIONS})`,
);
res.json({ message: "OK" });
} catch (error) {
console.error("⚠️ Upload endpoint exception occurred:", {
message: error.message,
stack: error.stack,
sessionCount: sessions.size,
timestamp: new Date().toISOString(),
});
// Provide more specific error messages based on error type
if (error.name === "SyntaxError") {
return res.status(400).json({
error: "Invalid JSON data format",
details: "The uploaded data could not be parsed as valid JSON",
});
} else if (error.message.includes("encrypt")) {
return res.status(500).json({
error: "Encryption failed",
details:
"Failed to encrypt session data. Please try again with a new API key.",
});
} else if (error.message.includes("PBKDF2")) {
return res.status(500).json({
error: "Key derivation failed",
details: "Failed to derive encryption key from API key",
});
} else {
return res.status(500).json({
error: "Upload processing failed",
details:
"An unexpected error occurred while processing your upload. Please try again.",
});
}
}
});
app.get("/api/v1/sample", (req, res) => {
try {
const apiKey = req.headers["x-api-key"];
// Validate API key header
if (!apiKey || !isValidApiKey(apiKey)) {
return res
.status(403)
.json({ error: "Invalid or missing X-API-Key header" });
}
const sessionId = getSessionId(apiKey);
const session = sessions.get(sessionId);
if (!session) {
return res.json(null);
}
// Decrypt data
const salt = Buffer.from(session.salt, "hex");
const key = deriveKey(apiKey, salt);
const decryptedData = decrypt(session.encryptedData, key);
// Remove session after first access (one-time use)
sessions.delete(sessionId);
console.log(
`📤 Sample data retrieved and session cleared: ${sessionId.substring(0, 8)}...`,
);
res.json(decryptedData);
} catch (error) {
console.error("⚠️ Sample retrieval exception occurred:", {
message: error.message,
stack: error.stack,
sessionCount: sessions.size,
timestamp: new Date().toISOString(),
});
// Provide more specific error messages based on error type
if (error.message.includes("decrypt")) {
return res.status(500).json({
error: "Decryption failed",
details:
"Failed to decrypt session data. The session may be corrupted or the API key may be incorrect.",
});
} else if (error.message.includes("JSON")) {
return res.status(500).json({
error: "Data corruption detected",
details:
"The stored session data appears to be corrupted and cannot be parsed.",
});
} else if (error.name === "TypeError") {
return res.status(500).json({
error: "Session data format error",
details: "The session data format is invalid or corrupted.",
});
} else {
return res.status(500).json({
error: "Sample retrieval failed",
details:
"An unexpected error occurred while retrieving sample data. The session may have been corrupted.",
});
}
}
});
app.get("/api/v1/state", (req, res) => {
try {
const apiKey = req.headers["x-api-key"];
// Validate API key header
if (!apiKey || !isValidApiKey(apiKey)) {
return res
.status(403)
.json({ error: "Invalid or missing X-API-Key header" });
}
const sessionId = getSessionId(apiKey);
const session = sessions.get(sessionId);
if (!session) {
// Return null state when no session exists
return res.json({ state: null });
}
res.json({ state: session.state });
} catch (error) {
console.error("⚠️ State retrieval exception occurred:", {
message: error.message,
stack: error.stack,
sessionCount: sessions.size,
timestamp: new Date().toISOString(),
});
// Provide more specific error messages
if (error.message.includes("API key")) {
return res.status(403).json({
error: "API key processing failed",
details: "Failed to process the provided API key",
});
} else {
return res.status(500).json({
error: "State retrieval failed",
details:
"An unexpected error occurred while retrieving session state. Please try again.",
});
}
}
});
// Status endpoint (no auth required) - detailed information
app.get("/api/v1/status", (req, res) => {
cleanupExpiredSessions(); // Cleanup on status check
res.json({
status: "healthy",
sessions: {
current: sessions.size,
max: MAX_SESSIONS,
available: MAX_SESSIONS - sessions.size,
},
limits: {
maxSessions: MAX_SESSIONS,
maxSampleSize: MAX_SAMPLE_SIZE,
maxSessionTTL: MAX_SESSION_TTL,
},
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
app.get("*", (req, res) => {
res.sendFile(path.join(__dirname, "build", "index.html"));
});
return app;
}
// Start server if this file is run directly
if (require.main === module) {
const { values } = parseArgs({
options: {
"listen-addr": {
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,
},
},
});
const DEV_MODE = values.dev;
const app = createApp(DEV_MODE);
const PORT = parseInt(values.port);
const HOST = values["listen-addr"];
app.listen(PORT, HOST, () => {
console.log(`JMESPath Playground Server running`);
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 Sample Size: ${(MAX_SAMPLE_SIZE / 1024 / 1024).toFixed(1)}MB`,
);
console.log(
` Session TTL: ${(MAX_SESSION_TTL / 1000 / 60).toFixed(0)} minutes`,
);
console.log(
" 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}`);
});
}
module.exports = { createApp };

View File

@@ -1,107 +1,46 @@
/* JMESPath Testing Tool Custom Styles */
/* JMESPath Testing Tool - Minimal Styles */
:root {
--font-sans: "Noto Sans", -apple-system, BlinkMacSystemFont, "Segoe UI",
"Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
"Helvetica Neue", sans-serif;
--font-mono: "JetBrains Mono", "Fira Code", "Noto Sans Mono", "Consolas", "Monaco", "Courier New", monospace;
}
/* Base font family */
body {
font-family: 'Noto Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
font-family: var(--font-sans);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
margin: 0;
transition: background-color 0.3s ease;
}
/* Layout structure */
.vh-100 {
height: 100vh;
#root {
display: flex;
flex-direction: column;
min-height: 100vh;
}
/* Header section styling - more compact */
.header-section {
/* Removed gradient background to fix text visibility */
/* Scrollbar styling for a cleaner look */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
/* Custom card styling */
.card {
border: none;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
border-radius: 8px;
::-webkit-scrollbar-track {
background: transparent;
}
.card-header {
background-color: #f8f9fa;
border-bottom: 2px solid #dee2e6;
font-weight: 600;
::-webkit-scrollbar-thumb {
background: rgba(0, 0, 0, 0.1);
border-radius: 4px;
}
/* Input and textarea styling */
.jmespath-input {
font-family: 'Noto Sans Mono', 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 14px;
padding: 10px;
font-weight: 400;
[data-mui-color-scheme="dark"] ::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
}
.json-input, .result-output {
font-family: 'Noto Sans Mono', 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 13px;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
font-weight: 400;
line-height: 1.4;
::-webkit-scrollbar-thumb:hover {
background: rgba(0, 0, 0, 0.2);
}
.json-input.error {
border-color: #dc3545;
background-color: #fff5f5;
}
.result-output.success {
border-color: #28a745;
background-color: #f0fff4;
}
/* Button styling */
.btn {
transition: all 0.2s ease;
}
.btn:hover {
transform: translateY(-1px);
}
/* Footer styling */
footer {
flex-shrink: 0;
}
footer a {
color: #6c757d;
transition: color 0.2s ease;
}
footer a:hover {
color: #495057;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.header-section {
padding: 1.5rem 0 !important;
}
.display-4 {
font-size: 2rem;
}
.lead {
font-size: 1rem;
}
.btn-sm {
font-size: 0.8rem;
padding: 0.25rem 0.5rem;
}
.card-body textarea {
min-height: 300px !important;
}
}

View File

@@ -1,351 +0,0 @@
import React, { useState, useEffect } from 'react';
import jmespath from 'jmespath';
import './App.css';
// JMESPath Testing Tool - Main Application Component
function App() {
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('');
const evaluateExpression = () => {
try {
// Clear previous errors
setError('');
setJsonError('');
// Validate and parse JSON
let parsedData;
try {
parsedData = JSON.parse(jsonData);
} catch (jsonErr) {
setJsonError(`Invalid JSON: ${jsonErr.message}`);
setResult('');
return;
}
// Evaluate JMESPath expression
const queryResult = jmespath.search(parsedData, jmespathExpression);
// Format the result
if (queryResult === null || queryResult === undefined) {
setResult('null');
} else {
setResult(JSON.stringify(queryResult, null, 2));
}
} catch (jmesErr) {
setError(`JMESPath Error: ${jmesErr.message}`);
setResult('');
}
};
// Auto-evaluate when inputs change
useEffect(() => {
if (jmespathExpression && jsonData) {
evaluateExpression();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [jmespathExpression, jsonData]);
const handleJmespathChange = (e) => {
setJmespathExpression(e.target.value);
};
const handleJsonChange = (e) => {
setJsonData(e.target.value);
};
const formatJson = () => {
try {
const parsed = JSON.parse(jsonData);
setJsonData(JSON.stringify(parsed, null, 2));
} catch (err) {
// If JSON is invalid, don't format
}
};
const clearAll = () => {
setJmespathExpression('');
setJsonData('');
setResult('');
setError('');
setJsonError('');
};
const loadSample = () => {
setJmespathExpression('people[*].name');
setJsonData(`{
"people": [
{
"name": "Alice Johnson",
"age": 28,
"city": "Chicago",
"skills": ["JavaScript", "React", "Node.js"]
},
{
"name": "Bob Wilson",
"age": 35,
"city": "Seattle",
"skills": ["Python", "Django", "PostgreSQL"]
},
{
"name": "Carol Davis",
"age": 32,
"city": "Austin",
"skills": ["Java", "Spring", "MySQL"]
}
],
"total": 3,
"department": "Engineering"
}`);
};
const loadFromDisk = () => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.json';
fileInput.onchange = (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const content = e.target.result;
// Handle .json files as regular JSON
JSON.parse(content); // Validate JSON
setJsonData(content);
setJsonError('');
} catch (err) {
setJsonError(`Invalid JSON file: ${err.message}`);
}
};
reader.readAsText(file);
}
};
fileInput.click();
};
const loadLogFile = () => {
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.log';
fileInput.onchange = (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const content = e.target.result;
const lines = content.split('\n')
.map(line => line.trim())
.filter(line => line.length > 0);
const jsonObjects = [];
for (const line of lines) {
try {
const obj = JSON.parse(line);
jsonObjects.push(obj);
} catch (lineError) {
throw new Error(`Invalid JSON on line: "${line.substring(0, 50)}..." - ${lineError.message}`);
}
}
const jsonContent = JSON.stringify(jsonObjects, null, 2);
setJsonData(jsonContent);
setJsonError('');
} catch (err) {
setJsonError(`Invalid log file: ${err.message}`);
}
};
reader.readAsText(file);
}
};
fileInput.click();
};
return (
<div className="container-fluid vh-100 d-flex flex-column">
{/* Top Section: Title only */}
<div className="header-section py-2">
<div className="container">
<div className="row">
<div className="col-12 text-center">
<h2 className="mb-1">JMESPath Testing Tool</h2>
</div>
</div>
</div>
</div>
{/* Main Content Section - flex-grow to fill space */}
<div className="container-fluid flex-grow-1 d-flex flex-column" style={{ minHeight: 0 }}>
{/* Description paragraph */}
<div className="row mb-2">
<div className="col-12">
<p className="text-muted text-center mb-2 small">
Validate and test JMESPath expressions against JSON data in real-time.
Enter your JMESPath query and JSON data below to see the results instantly.
</p>
</div>
</div>
{/* Middle Section: JMESPath Expression Input */}
<div className="row mb-2">
<div className="col-12">
<div className="card">
<div className="card-header d-flex justify-content-between align-items-center py-2">
<h6 className="mb-0">
<i className="bi bi-search me-2"></i>
JMESPath Expression
</h6>
<div>
<button
className="btn btn-outline-success btn-sm me-2"
onClick={loadFromDisk}
title="Load JSON object from file"
>
📄 Load an Object
</button>
<button
className="btn btn-outline-info btn-sm me-2"
onClick={loadLogFile}
title="Load JSON Lines log file"
>
📋 Load a Log File
</button>
<button
className="btn btn-outline-primary btn-sm me-2"
onClick={loadSample}
title="Load sample data"
>
Load Sample
</button>
<button
className="btn btn-outline-secondary btn-sm me-2"
onClick={formatJson}
title="Format JSON input for better readability"
>
Format JSON
</button>
<button
className="btn btn-outline-danger btn-sm"
onClick={clearAll}
title="Clear all inputs"
>
Clear All
</button>
</div>
</div>
<div className="card-body">
<input
type="text"
className={`form-control jmespath-input ${error ? 'error' : ''}`}
value={jmespathExpression}
onChange={handleJmespathChange}
placeholder="Enter JMESPath expression (e.g., people[*].name)"
/>
{error && (
<div className="alert alert-danger mt-2 mb-0">
<small>{error}</small>
</div>
)}
</div>
</div>
</div>
</div>
{/* Lower Middle Sections: JSON Data (left) and Query Result (right) */}
<div className="row flex-grow-1" style={{ minHeight: 0 }}>
{/* Lower Middle Left Section: JSON Data Input */}
<div className="col-md-6">
<div className="card h-100">
<div className="card-header py-2">
<h6 className="mb-0">
<i className="bi bi-file-code me-2"></i>
JSON Data
</h6>
</div>
<div className="card-body d-flex flex-column" style={{ minHeight: 0 }}>
<div className="flex-grow-1" style={{ minHeight: 0 }}>
<textarea
className={`form-control h-100 json-input ${jsonError ? 'error' : ''}`}
value={jsonData}
onChange={handleJsonChange}
placeholder="Enter JSON data here..."
style={{ minHeight: 0, resize: 'none' }}
/>
</div>
{jsonError && (
<div className="alert alert-danger mt-1 mb-0 py-1">
<small>{jsonError}</small>
</div>
)}
</div>
</div>
</div>
{/* Lower Middle Right Section: Query Results Output */}
<div className="col-md-6">
<div className="card h-100">
<div className="card-header py-2">
<h6 className="mb-0">
<i className="bi bi-arrow-right-circle me-2"></i>
Query Result
</h6>
</div>
<div className="card-body d-flex flex-column" style={{ minHeight: 0 }}>
<div className="flex-grow-1" style={{ minHeight: 0 }}>
<textarea
className={`form-control h-100 result-output ${result && !error && !jsonError ? 'success' : ''}`}
value={result}
readOnly
placeholder="Results will appear here..."
style={{ minHeight: 0, resize: 'none' }}
/>
</div>
</div>
</div>
</div>
</div>
</div>
{/* Bottom Section: Footer */}
<footer className="bg-light border-top mt-2 py-2 flex-shrink-0">
<div className="container">
<div className="row">
<div className="col-md-6">
<p className="mb-0 text-muted small">
<strong>JMESPath Testing Tool</strong> - Created for testing and validating JMESPath expressions
</p>
</div>
<div className="col-md-6 text-md-end">
<p className="mb-0 text-muted small">
Licensed under <a href="#" className="text-decoration-none">MIT License</a> |
<a href="https://jmespath.org/" target="_blank" rel="noopener noreferrer" className="text-decoration-none ms-2">
Learn JMESPath
</a>
</p>
</div>
</div>
</div>
</footer>
</div>
);
}
export default App;

221
src/App.jsx Normal file
View File

@@ -0,0 +1,221 @@
import React, { useState, useEffect } from "react";
import {
CssBaseline,
Box,
useColorScheme,
} from "@mui/material";
import Header from "./components/Header";
import Footer from "./components/Footer";
import MainPage from "./components/MainPage";
import ApiKeyPage from "./components/ApiKeyPage";
import "./App.css";
// Utility function to generate a cryptographically secure API key
function generateApiKey() {
const array = new Uint8Array(16);
// Use crypto.getRandomValues if available (browser), fallback for tests
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
crypto.getRandomValues(array);
} else {
// Fallback for test environments - not cryptographically secure
for (let i = 0; i < array.length; i++) {
array[i] = Math.floor(Math.random() * 256);
}
}
return Array.from(array, (byte) => byte.toString(16).padStart(2, "0")).join(
"",
);
}
// JMESPath Testing Tool - Main Application Component
function App() {
const [currentPage, setCurrentPage] = useState("main"); // 'main' or 'apikey'
const [theme, setTheme] = useState(() => {
// Load theme from localStorage or default to '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 [currentStateGuid, setCurrentStateGuid] = 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(() => {
// Load API key from localStorage or generate new one
const stored = localStorage.getItem("jmespath-api-key");
if (stored && /^[0-9a-f]{32}$/i.test(stored)) {
return stored;
}
const newKey = generateApiKey();
localStorage.setItem("jmespath-api-key", newKey);
return newKey;
});
const getApiHeaders = () => ({
"Accept": "application/json",
"x-api-key": apiKey,
});
const { setMode } = useColorScheme();
// Load sample data from API on startup and setup periodic state checking
useEffect(() => {
// Sync initial theme from localStorage with MUI color scheme
const initialMode = theme === 'auto' ? 'system' : theme;
setMode(initialMode);
loadSampleData();
// Check for state changes every 5 seconds
const interval = setInterval(checkStateChange, 5000);
return () => clearInterval(interval);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [apiKey]);
// Check if state has changed (new data uploaded)
const checkStateChange = async () => {
try {
const response = await fetch("/api/v1/state", {
headers: getApiHeaders(),
});
if (response.ok) {
const stateData = await response.json();
if (stateData.state && stateData.state !== currentStateGuid) {
setShowReloadButton(true);
}
}
} catch (error) {
// Silently handle state check errors
}
};
// Load sample data from API
const loadSampleData = async () => {
try {
setShowReloadButton(false);
const response = await fetch("/api/v1/sample", {
headers: getApiHeaders(),
});
if (response.ok) {
const data = await response.json();
if (data) {
setJsonData(JSON.stringify(data, null, 2));
}
// Update current state GUID
const stateResponse = await fetch("/api/v1/state", {
headers: getApiHeaders(),
});
if (stateResponse.ok) {
const stateData = await stateResponse.json();
setCurrentStateGuid(stateData.state);
}
}
} catch (error) {
console.error("Failed to load sample data:", error);
}
};
// Regenerate API key
const regenerateApiKey = () => {
const newKey = generateApiKey();
setApiKey(newKey);
localStorage.setItem("jmespath-api-key", newKey);
setShowReloadButton(false);
setCurrentStateGuid(null);
};
const handleThemeChange = (newTheme) => {
setTheme(newTheme);
const muiMode = newTheme === "auto" ? "system" : newTheme;
setMode(muiMode);
localStorage.setItem("theme", newTheme);
};
const handlePageChange = (newPage) => {
setCurrentPage(newPage);
};
const handleShellTypeChange = (newShellType) => {
setShellType(newShellType);
localStorage.setItem("jmespath-shell-type", newShellType);
};
return (
<>
<CssBaseline />
<Box
sx={{
display: "flex",
flexDirection: "column",
height: "100vh",
overflow: "hidden",
bgcolor: "background.default",
}}
>
<Header
theme={theme}
onThemeChange={handleThemeChange}
currentPage={currentPage}
onPageChange={handlePageChange}
/>
{/* Main Content Section - flex-grow to fill space */}
<Box
component="main"
sx={{
flexGrow: 1,
display: "flex",
flexDirection: "column",
minHeight: 0,
height: "100%", // Force height for children
}}
>
{currentPage === "main" ? (
<MainPage
apiKey={apiKey}
showReloadButton={showReloadButton}
onReloadSampleData={loadSampleData}
jmespathExpression={jmespathExpression}
setJmespathExpression={setJmespathExpression}
jsonData={jsonData}
setJsonData={setJsonData}
/>
) : (
<ApiKeyPage
apiKey={apiKey}
onRegenerateApiKey={regenerateApiKey}
shellType={shellType}
onShellTypeChange={handleShellTypeChange}
/>
)}
</Box>
<Footer />
</Box>
</>
);
}
export default App;

View File

@@ -1,22 +0,0 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders JMESPath Testing Tool title', () => {
render(<App />);
const titleElement = screen.getByText(/JMESPath Testing Tool/i);
expect(titleElement).toBeInTheDocument();
});
test('renders input areas', () => {
render(<App />);
const jmespathInput = screen.getByPlaceholderText(/Enter JMESPath expression/i);
const jsonInput = screen.getByPlaceholderText(/Enter JSON data here/i);
expect(jmespathInput).toBeInTheDocument();
expect(jsonInput).toBeInTheDocument();
});
test('renders result area', () => {
render(<App />);
const resultArea = screen.getByPlaceholderText(/Results will appear here/i);
expect(resultArea).toBeInTheDocument();
});

376
src/App.test.jsx Normal file
View File

@@ -0,0 +1,376 @@
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
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
global.fetch = vi.fn();
describe('App Component', () => {
beforeEach(() => {
vi.clearAllMocks();
// Mock successful API responses
fetch.mockImplementation((url) => {
if (url.includes('/api/v1/sample')) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({
"people": [
{ "name": "John Doe", "age": 30, "city": "New York" },
{ "name": "Jane Smith", "age": 25, "city": "Los Angeles" }
],
"total": 2
})
});
}
if (url.includes('/api/v1/state')) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ state: 'test-state-123' })
});
}
return Promise.reject(new Error('Unknown URL'));
});
});
describe('Basic Rendering', () => {
test('renders JMESPath Testing Tool title', () => {
render(<App />);
const titleElement = screen.getByText(/JMESPath Playground/i);
expect(titleElement).toBeInTheDocument();
});
test('renders input areas', () => {
render(<App />);
const jmespathInput = screen.getByPlaceholderText(/Enter JMESPath expression/i);
const jsonInput = screen.getByPlaceholderText(/Enter JSON data here/i);
expect(jmespathInput).toBeInTheDocument();
expect(jsonInput).toBeInTheDocument();
});
test('renders result area', () => {
render(<App />);
const resultArea = screen.getByPlaceholderText(/Results will appear here/i);
expect(resultArea).toBeInTheDocument();
});
test('renders version number', () => {
render(<App />);
// 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();
// Check if it's a dev/test/unknown build
const isDevBuild = versionText.textContent.includes('-dev') ||
versionText.textContent.includes('-test') ||
versionText.textContent.includes('unknown');
// Additional validations can be added here based on build type
if (isDevBuild) {
// Dev/test/unknown specific validations
expect(versionText.textContent).toMatch(/(v\d+\.\d+\.\d+-(dev|test)|unknown)/);
} else {
// Release build validations - just check that version pattern exists in the text
expect(versionText.textContent).toMatch(/v\d+\.\d+\.\d+/);
}
});
test('renders all toolbar buttons', () => {
render(<App />);
expect(screen.getByRole('button', { name: /Load from Disk/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Load Logs/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Load Sample/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Format/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Clear all inputs/i })).toBeInTheDocument();
});
});
describe('JMESPath Functionality', () => {
test('evaluates simple JMESPath expression', async () => {
const user = userEvent.setup();
render(<App />);
const jmespathInput = screen.getByPlaceholderText(/Enter JMESPath expression/i);
const jsonInput = screen.getByPlaceholderText(/Enter JSON data here/i);
const resultArea = screen.getByPlaceholderText(/Results will appear here/i);
// Clear all inputs first to start fresh
const clearButton = screen.getByRole('button', { name: /Clear all inputs/i });
await user.click(clearButton);
// Set JSON data directly after clearing
fireEvent.change(jsonInput, { target: { value: '{"name": "Alice", "age": 30}' } });
// Enter JMESPath expression after a small delay to ensure JSON is processed
await user.clear(jmespathInput);
await user.type(jmespathInput, 'name');
// Check result - use waitFor with more relaxed expectations
await waitFor(() => {
expect(resultArea.value).toMatch(/"Alice"|Alice/);
}, { timeout: 3000 });
});
test('handles invalid JMESPath expression', async () => {
const user = userEvent.setup();
render(<App />);
const jmespathInput = screen.getByPlaceholderText(/Enter JMESPath expression/i);
const jsonInput = screen.getByPlaceholderText(/Enter JSON data here/i);
// Set valid JSON directly
fireEvent.change(jsonInput, { target: { value: '{"name": "Alice"}' } });
// Enter invalid JMESPath expression without special characters that user-event can't parse
await user.clear(jmespathInput);
await user.type(jmespathInput, 'invalid.expression.');
// Should show error state
await waitFor(() => {
const errorAlert = screen.getByText(/JMESPath Error:/i);
expect(errorAlert).toBeInTheDocument();
});
});
test('handles invalid JSON input', async () => {
const user = userEvent.setup();
render(<App />);
const jmespathInput = screen.getByPlaceholderText(/Enter JMESPath expression/i);
const jsonInput = screen.getByPlaceholderText(/Enter JSON data here/i);
// Clear all inputs first
const clearButton = screen.getByRole('button', { name: /Clear all inputs/i });
await user.click(clearButton);
// Set invalid JSON directly
fireEvent.change(jsonInput, { target: { value: '{invalid json}' } });
// Enter valid JMESPath expression
await user.clear(jmespathInput);
await user.type(jmespathInput, 'name');
// Should show JSON error indicator - check for error styling or messages
await waitFor(() => {
const jsonInputWithError = document.querySelector('.json-input.error') ||
document.querySelector('.json-input.is-invalid') ||
screen.queryByText(/Unexpected token/i) ||
screen.queryByText(/JSON Error:/i) ||
screen.queryByText(/Invalid JSON:/i) ||
screen.queryByText(/SyntaxError/i);
// If no specific error styling/message, at least ensure the result doesn't contain valid JSON result
if (!jsonInputWithError) {
const resultArea = screen.getByPlaceholderText(/Results will appear here/i);
expect(resultArea.value).not.toMatch(/"Alice"/); // Should not have valid result
} else {
expect(jsonInputWithError).toBeTruthy();
}
}, { timeout: 2000 });
});
});
describe('Theme Functionality', () => {
test('renders theme switcher buttons', () => {
render(<App />);
expect(screen.getByRole('button', { name: /Auto/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Light/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Dark/i })).toBeInTheDocument();
});
test('switches to light theme when clicked', async () => {
const user = userEvent.setup();
render(<App />);
const lightButton = screen.getByRole('button', { name: /Light/i });
await user.click(lightButton);
// Check if button becomes active
expect(lightButton).toHaveClass('Mui-selected');
});
test('switches to dark theme when clicked', async () => {
const user = userEvent.setup();
render(<App />);
const darkButton = screen.getByRole('button', { name: /Dark/i });
await user.click(darkButton);
// Check if button becomes active
expect(darkButton).toHaveClass('Mui-selected');
});
});
describe('Navigation', () => {
test('switches to API Keys page and back', async () => {
const user = userEvent.setup();
render(<App />);
// Find and click API Keys button in Header
// MUI Tooltip might set aria-label to title "API Key Management"
const apiKeyButton = screen.getByRole('button', { name: /API Key Management/i });
await user.click(apiKeyButton);
// Check if API Key Management title is visible
expect(screen.getByText(/API Key Management/i)).toBeInTheDocument();
expect(screen.getByText(/YOUR API KEY/i)).toBeInTheDocument();
// Find and click Home button to go back
// MUI Tooltip title "Back to Testing" becomes the accessible name
const homeButton = screen.getByRole('button', { name: /Back to Testing/i });
await user.click(homeButton);
// Check if we are back on main page
expect(screen.getByRole('heading', { name: /JMESPath Expression/i })).toBeInTheDocument();
});
});
describe('Toolbar Actions', () => {
test('clear all button clears inputs', async () => {
const user = userEvent.setup();
render(<App />);
const jmespathInput = screen.getByPlaceholderText(/Enter JMESPath expression/i);
const jsonInput = screen.getByPlaceholderText(/Enter JSON data here/i);
const clearButton = screen.getByRole('button', { name: /Clear all inputs/i });
// Add some content
await user.type(jmespathInput, 'test.expression');
fireEvent.change(jsonInput, { target: { value: '{"test": "data"}' } });
// Clear all
await user.click(clearButton);
// Check inputs are cleared
expect(jmespathInput.value).toBe('');
expect(jsonInput.value).toBe('');
});
test('format JSON button formats JSON input', async () => {
const user = userEvent.setup();
render(<App />);
const jsonInput = screen.getByPlaceholderText(/Enter JSON data here/i);
const formatButton = screen.getByRole('button', { name: "Format" });
// Add minified JSON directly
fireEvent.change(jsonInput, { target: { value: '{"name":"Alice","age":30,"skills":["React","Node"]}' } });
// Format JSON
await user.click(formatButton);
// Check if JSON is formatted (contains newlines and indentation)
await waitFor(() => {
expect(jsonInput.value).toContain('\n');
expect(jsonInput.value).toContain(' '); // indentation
});
});
test('load sample button loads default data', async () => {
const user = userEvent.setup();
render(<App />);
const loadSampleButton = screen.getByRole('button', { name: "Load Sample" });
const jsonInput = screen.getByPlaceholderText(/Enter JSON data here/i);
const jmespathInput = screen.getByPlaceholderText(/Enter JMESPath expression/i);
// Clear inputs first
fireEvent.change(jsonInput, { target: { value: '' } });
fireEvent.change(jmespathInput, { target: { value: '' } });
// Load sample
await user.click(loadSampleButton);
// Check if sample data is loaded (adjust expectations based on actual API response)
await waitFor(() => {
expect(jsonInput.value).toContain('users');
// The default sample loads users[?age > `30`].name
expect(jmespathInput.value).toBe('users[?age > `30`].name');
}, { timeout: 2000 });
});
});
describe('API Integration', () => {
test('loads sample data from API on mount', async () => {
render(<App />);
// Wait for API calls to complete - the app calls sample endpoint first
await waitFor(() => {
expect(fetch).toHaveBeenCalledWith('/api/v1/sample', expect.objectContaining({
headers: expect.objectContaining({
'Accept': 'application/json'
})
}));
});
// The app may not call sample endpoint immediately on mount in all scenarios
// We just verify that the state endpoint is called for API polling
});
test('shows reload button when state changes', async () => {
// Mock different state on subsequent calls
fetch.mockImplementation((url, options) => {
if (url.includes('/api/v1/state')) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ state: 'different-state-456' })
});
}
if (url.includes('/api/v1/sample')) {
return Promise.resolve({
ok: true,
json: () => Promise.resolve({ "test": "data" })
});
}
return Promise.reject(new Error('Unknown URL'));
});
render(<App />);
// Wait for potential reload button to appear
await waitFor(() => {
// This test might need adjustment based on actual implementation
// For now, we just verify the API calls are made
expect(fetch).toHaveBeenCalled();
}, { timeout: 3000 });
});
});
describe('File Input Handling', () => {
test('handles file input for JSON object', async () => {
const user = userEvent.setup();
render(<App />);
const loadObjectButton = screen.getByRole('button', { name: "Load from Disk" });
// Create a mock file
const file = new File(['{"test": "file data"}'], 'test.json', {
type: 'application/json',
});
// Mock the file input
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.json';
// We can't easily test file upload without more setup,
// but we can verify the button exists and is clickable
expect(loadObjectButton).toBeInTheDocument();
await user.click(loadObjectButton);
});
});
});

View File

@@ -0,0 +1,205 @@
import React, { useState } from "react";
import {
Box,
Typography,
Paper,
TextField,
Button,
Grid,
Tooltip,
IconButton,
ToggleButtonGroup,
ToggleButton,
Divider,
} from "@mui/material";
import {
ContentCopy as ContentCopyIcon,
Autorenew as AutorenewIcon,
Check as CheckIcon,
Key as KeyIcon,
} from "@mui/icons-material";
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 (
<Box sx={{ position: "relative", my: 2 }}>
<Paper
variant="outlined"
sx={{
p: 2,
pr: 6,
bgcolor: "action.hover",
fontFamily: "'Noto Sans Mono', monospace",
fontSize: "0.85rem",
whiteSpace: "pre-wrap",
wordBreak: "break-all",
position: "relative",
borderColor: "divider",
}}
>
<code>{code}</code>
<Tooltip title={copySuccess ? "Copied!" : "Copy code"}>
<IconButton
size="small"
onClick={handleCopy}
sx={{
position: "absolute",
top: 8,
right: 8,
color: copySuccess ? "success.main" : "primary.main",
}}
>
{copySuccess ? (
<CheckIcon fontSize="small" />
) : (
<ContentCopyIcon fontSize="small" />
)}
</IconButton>
</Tooltip>
</Paper>
</Box>
);
}
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);
}
};
return (
<Box sx={{ flexGrow: 1, py: 4, px: 2 }}>
<Grid container justifyContent="center">
<Grid size={{ xs: 12, md: 10, lg: 8 }}>
<Paper elevation={1} sx={{ p: { xs: 3, md: 5 }, bgcolor: "background.paper", border: 1, borderColor: "divider" }}>
<Typography variant="h5" gutterBottom sx={{ mb: 4, fontWeight: 700, display: "flex", alignItems: "center", gap: 1.5, color: "text.primary" }}>
<KeyIcon color="primary" /> API Key Management
</Typography>
<Box sx={{ mb: 6 }}>
<Typography variant="subtitle2" gutterBottom color="text.secondary">
YOUR API KEY
</Typography>
<Box sx={{ display: "flex", gap: 1.5, alignItems: "center" }}>
<TextField
fullWidth
value={apiKey}
slotProps={{
input: {
readOnly: true,
style: { fontFamily: "'Noto Sans Mono', monospace", fontSize: "0.9rem" },
},
}}
variant="outlined"
sx={{ "& .MuiOutlinedInput-root": { bgcolor: "background.paper" } }}
/>
<Tooltip title="Copy API Key">
<IconButton
onClick={handleCopyToClipboard}
color={copySuccess ? "success" : "primary"}
size="medium"
sx={{ border: 1, borderColor: "divider" }}
>
{copySuccess ? <CheckIcon /> : <ContentCopyIcon />}
</IconButton>
</Tooltip>
<Tooltip title="Regenerate Key">
<IconButton
onClick={onRegenerateApiKey}
color="primary"
size="medium"
sx={{ border: 1, borderColor: "divider" }}
>
<AutorenewIcon />
</IconButton>
</Tooltip>
</Box>
<Typography variant="caption" color="text.secondary" sx={{ mt: 1.5, display: "block" }}>
This key is stored locally in your browser. Use it to authenticate remote data uploads.
</Typography>
</Box>
<Divider sx={{ my: 4, borderColor: "divider" }} />
<Box sx={{ mb: 4 }}>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
mb: 3,
flexWrap: "wrap",
gap: 2,
}}
>
<Typography variant="h6" fontWeight="600" color="text.primary">Remote Upload API</Typography>
<ToggleButtonGroup
size="small"
value={shellType}
exclusive
onChange={(e, value) => value && onShellTypeChange(value)}
aria-label="shell type"
sx={{ "& .MuiToggleButton-root": { px: 2, py: 0.5 } }}
>
<ToggleButton value="bash">UNIX (Bash)</ToggleButton>
<ToggleButton value="powershell">Windows (PS)</ToggleButton>
</ToggleButtonGroup>
</Box>
<Typography variant="body2" color="text.secondary" paragraph>
Use this endpoint to upload data from external scripts. Set these environment variables:
</Typography>
<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}"`
}
/>
<CodeBlock
code={
shellType === "bash"
? `curl -X POST "$JMESPATH_PLAYGROUND_API_URL/api/v1/upload" \\
-H "Accept: application/json" \\
-H "x-api-key: $JMESPATH_PLAYGROUND_API_KEY" \\
-d '{ "users": [ { "id": 1, "name": "Remote User" } ] }'`
: `Invoke-RestMethod -Method Post -Uri "$env:JMESPATH_PLAYGROUND_API_URL/api/v1/upload" \`
-Headers @{ "Accept" = "application/json"; "x-api-key" = $env:JMESPATH_PLAYGROUND_API_KEY } \`
-Body '{ "users": [ { "id": 1, "name": "Remote User" } ] }'`
}
/>
</Box>
</Paper>
</Grid>
</Grid>
</Box>
);
}
export default ApiKeyPage;

58
src/components/Footer.jsx Normal file
View File

@@ -0,0 +1,58 @@
import React from "react";
import { Box, Typography, Container, Link, Grid } from "@mui/material";
import { VERSION } from "../version";
function Footer() {
return (
<Box
component="footer"
sx={{
py: 1,
borderTop: 1,
borderColor: "divider",
bgcolor: "background.paper",
flexShrink: 0,
}}
>
<Container maxWidth="xl">
<Grid container spacing={2} alignItems="center">
<Grid size={{ xs: 12, md: 6 }}>
<Typography variant="body2" color="text.secondary">
<strong>JMESPath Testing Tool</strong>{" "}
{VERSION === "unknown" ? VERSION : `v${VERSION}`} - Created for
testing and validating JMESPath expressions
</Typography>
</Grid>
<Grid size={{ xs: 12, md: 6 }} sx={{ textAlign: { md: "right" } }}>
<Typography variant="body2" color="text.secondary">
Licensed under{" "}
<Link
href="https://opensource.org/licenses/MIT"
target="_blank"
rel="noopener noreferrer"
color="primary"
underline="hover"
sx={{ fontWeight: 500 }}
>
MIT License
</Link>{" "}
|{" "}
<Link
href="https://jmespath.org/"
target="_blank"
rel="noopener noreferrer"
color="primary"
underline="hover"
sx={{ ml: 1, fontWeight: 500 }}
>
Learn JMESPath
</Link>
</Typography>
</Grid>
</Grid>
</Container>
</Box>
);
}
export default Footer;

89
src/components/Header.jsx Normal file
View File

@@ -0,0 +1,89 @@
import React from "react";
import {
Box,
Typography,
Button,
ToggleButton,
ToggleButtonGroup,
Tooltip,
AppBar,
Toolbar,
Container,
Divider,
} from "@mui/material";
import KeyIcon from "@mui/icons-material/Key";
import HomeIcon from "@mui/icons-material/Home";
import BrightnessAutoIcon from "@mui/icons-material/BrightnessAuto";
import LightModeIcon from "@mui/icons-material/LightMode";
import DarkModeIcon from "@mui/icons-material/DarkMode";
function Header({ theme, onThemeChange, currentPage, onPageChange }) {
return (
<AppBar position="static" color="default" elevation={1} sx={{ borderBottom: 1, borderColor: "divider" }}>
<Container maxWidth="xl">
<Toolbar disableGutters sx={{ display: "flex", justifyContent: "space-between", height: 64 }}>
{/* Brand/Title */}
<Box sx={{ display: "flex", alignItems: "center" }}>
<Typography
variant="h5"
noWrap
component="div"
sx={{
fontWeight: 700,
color: "primary.main",
letterSpacing: ".05rem",
}}
>
JMESPath Playground
</Typography>
</Box>
{/* Right side controls */}
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
{/* API Key Management Button */}
<Tooltip title={currentPage === "main" ? "API Key Management" : "Back to Testing"}>
<Button
variant={currentPage === "apikey" ? "contained" : "text"}
color={currentPage === "apikey" ? "primary" : "primary"}
size="medium"
startIcon={currentPage === "main" ? <KeyIcon /> : <HomeIcon />}
onClick={() => onPageChange(currentPage === "main" ? "apikey" : "main")}
>
{currentPage === "main" ? "API Keys" : "Home"}
</Button>
</Tooltip>
<Divider orientation="vertical" flexItem sx={{ my: 2, mx: 1 }} />
{/* Theme switcher */}
<ToggleButtonGroup
value={theme}
exclusive
onChange={(e, nextTheme) => nextTheme && onThemeChange(nextTheme)}
aria-label="theme switcher"
size="small"
>
<Tooltip title="Follow system theme">
<ToggleButton value="auto" aria-label="Auto">
<BrightnessAutoIcon sx={{ fontSize: "1.2rem" }} />
</ToggleButton>
</Tooltip>
<Tooltip title="Light mode">
<ToggleButton value="light" aria-label="Light">
<LightModeIcon sx={{ fontSize: "1.2rem" }} />
</ToggleButton>
</Tooltip>
<Tooltip title="Dark mode">
<ToggleButton value="dark" aria-label="Dark">
<DarkModeIcon sx={{ fontSize: "1.2rem" }} />
</ToggleButton>
</Tooltip>
</ToggleButtonGroup>
</Box>
</Toolbar>
</Container>
</AppBar>
);
}
export default Header;

535
src/components/MainPage.jsx Normal file
View File

@@ -0,0 +1,535 @@
import React, { useState, useEffect } from "react";
import {
Box,
Typography,
Paper,
TextField,
Button,
Tooltip,
IconButton,
Alert,
Stack,
Divider,
} from "@mui/material";
import {
Search as SearchIcon,
DataObject as DataObjectIcon,
Output as OutputIcon,
UploadFile as UploadFileIcon,
FileOpen as FileOpenIcon,
Restore as RestoreIcon,
FormatAlignLeft as FormatAlignLeftIcon,
Clear as ClearIcon,
ContentCopy as ContentCopyIcon,
Download as DownloadIcon,
Check as CheckIcon,
Refresh as RefreshIcon,
} from "@mui/icons-material";
import Grid from "@mui/material/Grid";
import jmespath from "jmespath";
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 = () => {
try {
// Clear previous errors
setError("");
setJsonError("");
// Validate and parse JSON
let parsedData;
try {
parsedData = JSON.parse(jsonData);
} catch (jsonErr) {
setJsonError(`Invalid JSON: ${jsonErr.message}`);
setResult("");
return;
}
// Evaluate JMESPath expression
const queryResult = jmespath.search(parsedData, jmespathExpression);
// Format the result
if (queryResult === null || queryResult === undefined) {
setResult("null");
} else {
setResult(JSON.stringify(queryResult, null, 2));
}
} catch (jmesErr) {
setError(`JMESPath Error: ${jmesErr.message}`);
setResult("");
}
};
// Auto-evaluate when inputs change
useEffect(() => {
if (jmespathExpression && jsonData) {
evaluateExpression();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [jmespathExpression, jsonData]);
const handleJmespathChange = (e) => {
setJmespathExpression(e.target.value);
};
const handleJsonChange = (e) => {
setJsonData(e.target.value);
};
const formatJson = () => {
try {
const parsed = JSON.parse(jsonData);
setJsonData(JSON.stringify(parsed, null, 2));
} catch (err) {
// If JSON is invalid, don't format
}
};
const clearAll = () => {
setJmespathExpression("");
setJsonData("");
setResult("");
setError("");
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 sampleData = {
users: [
{ name: "Alice", age: 30, city: "New York" },
{ name: "Bob", age: 25, city: "San Francisco" },
{ name: "Charlie", age: 35, city: "Chicago" },
],
total: 3,
};
setJsonData(JSON.stringify(sampleData, null, 2));
setJmespathExpression("users[?age > `30`].name");
};
const loadFromDisk = () => {
const input = document.createElement("input");
input.type = "file";
input.accept = ".json";
input.onchange = (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const content = e.target.result;
const parsed = JSON.parse(content);
setJsonData(JSON.stringify(parsed, null, 2));
} catch (error) {
alert("Invalid JSON file");
}
};
reader.readAsText(file);
}
};
input.click();
};
const loadLogFile = () => {
const input = document.createElement("input");
input.type = "file";
input.accept = ".log,.jsonl,.ndjson";
input.onchange = (e) => {
const file = e.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
try {
const content = e.target.result;
const lines = content.trim().split("\n");
const logs = lines.map((line) => JSON.parse(line));
setJsonData(JSON.stringify(logs, null, 2));
setJmespathExpression("[*].message");
} catch (error) {
alert("Invalid JSON Lines file");
}
};
reader.readAsText(file);
}
};
input.click();
};
return (
<Box
sx={{
flexGrow: 1,
pt: 1,
pb: 3,
px: { xs: 2, md: 4 },
display: "flex",
flexDirection: "column",
minHeight: 0,
overflow: "hidden",
}}
>
<Box sx={{ mb: 2, flexShrink: 0 }}>
<Typography
variant="body2"
color="text.secondary"
align="left"
mt="1rem"
>
Validate and test JMESPath expressions against JSON data in real-time.
Enter your JMESPath query and JSON data below to see the results
instantly.
</Typography>
</Box>
<Paper
sx={{
mb: 1,
flexShrink: 0,
bgcolor: "background.paper",
border: 1,
borderColor: "divider",
overflow: "hidden",
mb: 2
}}
>
<Box
sx={{
px: 2,
py: 1,
bgcolor: "action.hover",
borderBottom: 1,
borderColor: "divider",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
}}
>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<SearchIcon sx={{ fontSize: 20 }} color="primary" />
<Typography variant="subtitle2" color="text.primary">
JMESPath Expression
</Typography>
</Box>
</Box>
<Box sx={{ p: 1.5, mt: 0.5 }}>
<TextField
fullWidth
size="small"
placeholder="Enter JMESPath expression (e.g., people[*].name)"
value={jmespathExpression}
onChange={handleJmespathChange}
error={!!error}
helperText={error || " "}
sx={{
"& .MuiInputBase-root": {
fontFamily: "'Noto Sans Mono', monospace",
fontSize: "0.9rem",
},
"& .MuiFormHelperText-root": {
mt: 0.75,
mb: -0.5,
},
}}
/>
</Box>
</Paper>
<Grid container spacing={3} sx={{ flex: "1 1 0", minHeight: 0, height: 0 }}>
<Grid size={{ xs: 12, md: 6 }} sx={{ display: "flex", flexDirection: "column", minHeight: 0 }}>
<Paper
sx={{
flexGrow: 1,
display: "flex",
flexDirection: "column",
overflow: "hidden",
bgcolor: "background.paper",
border: 1,
borderColor: "divider",
minHeight: 0,
}}
>
<Box
sx={{
px: 2,
py: 1,
bgcolor: "action.hover",
borderBottom: 1,
borderColor: "divider",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
<DataObjectIcon sx={{ fontSize: 20 }} color="primary" />
<Typography variant="subtitle2" color="text.primary">
JSON Input
</Typography>
{showReloadButton && (
<Button
variant="contained"
color="secondary"
onClick={onReloadSampleData}
startIcon={<RefreshIcon fontSize="inherit" />}
size="small"
sx={{
ml: 1,
px: 1,
py: 0.25,
fontSize: "0.65rem",
textTransform: "none",
whiteSpace: "nowrap",
minWidth: "auto",
}}
>
Reload data
</Button>
)}
</Box>
<Stack direction="row" spacing={1} alignItems="center">
<Tooltip title="Load from Disk">
<IconButton
size="small"
onClick={loadFromDisk}
color="primary"
aria-label="Load from Disk"
>
<FileOpenIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Load Logs">
<IconButton
size="small"
onClick={loadLogFile}
color="primary"
aria-label="Load Logs"
>
<UploadFileIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Load Sample">
<IconButton
size="small"
onClick={loadSample}
color="primary"
aria-label="Load Sample"
>
<RestoreIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip title="Format">
<IconButton
size="small"
onClick={formatJson}
color="primary"
aria-label="Format"
>
<FormatAlignLeftIcon fontSize="small" />
</IconButton>
</Tooltip>
<Divider orientation="vertical" flexItem sx={{ mx: 0.5 }} />
<Tooltip title="Clear all inputs">
<IconButton
size="small"
onClick={clearAll}
color="secondary"
aria-label="Clear all inputs"
>
<ClearIcon fontSize="small" />
</IconButton>
</Tooltip>
</Stack>
</Box>
<Box sx={{ p: 2, flex: "1 1 0", display: "flex", flexDirection: "column", minHeight: 0, overflow: "hidden" }}>
<TextField
multiline
fullWidth
value={jsonData}
onChange={handleJsonChange}
placeholder="Enter JSON data here..."
variant="standard"
slotProps={{
input: {
disableUnderline: true,
style: {
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
fontSize: "0.85rem",
lineHeight: 1.5,
height: "100%",
boxSizing: "border-box",
},
},
}}
sx={{
flex: "1 1 0",
display: "flex",
flexDirection: "column",
height: 0,
minHeight: 0,
"& .MuiInputBase-root": {
flex: "1 1 0",
display: "flex",
flexDirection: "column",
alignItems: "stretch",
height: "100%",
minHeight: 0,
},
"& .MuiInputBase-input": {
flexGrow: 1,
overflow: "auto !important",
height: "100% !important",
resize: "none",
padding: 0,
},
}}
/>
{jsonError && (
<Alert severity="error" sx={{ mt: 1, flexShrink: 0 }} variant="filled">
{jsonError}
</Alert>
)}
</Box>
</Paper>
</Grid>
<Grid size={{ xs: 12, md: 6 }} sx={{ display: "flex", flexDirection: "column", minHeight: 0 }}>
<Paper
sx={{
flexGrow: 1,
display: "flex",
flexDirection: "column",
overflow: "hidden",
bgcolor: "background.paper",
border: 1,
borderColor: "divider",
minHeight: 0,
}}
>
<Box
sx={{
px: 2,
py: 1,
bgcolor: "action.hover",
borderBottom: 1,
borderColor: "divider",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Box sx={{ display: "flex", alignItems: "center" }}>
<OutputIcon sx={{ mr: 1, fontSize: 20 }} color="primary" />
<Typography variant="subtitle2" color="text.primary">
Query Result
</Typography>
</Box>
<Stack direction="row" spacing={1}>
<Tooltip title="Copy to Clipboard">
<span>
<IconButton
size="small"
onClick={copyToClipboard}
disabled={!result || result === "null"}
color={copySuccess ? "success" : "primary"}
>
{copySuccess ? <CheckIcon fontSize="small" /> : <ContentCopyIcon fontSize="small" />}
</IconButton>
</span>
</Tooltip>
<Tooltip title="Download Result">
<span>
<IconButton
size="small"
onClick={downloadResult}
disabled={!result || result === "null"}
color="primary"
>
<DownloadIcon fontSize="small" />
</IconButton>
</span>
</Tooltip>
</Stack>
</Box>
<Box sx={{ p: 2, flex: "1 1 0", display: "flex", flexDirection: "column", minHeight: 0, overflow: "hidden" }}>
<TextField
multiline
fullWidth
value={result}
variant="standard"
placeholder="Results will appear here..."
slotProps={{
input: {
readOnly: true,
disableUnderline: true,
style: {
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
fontSize: "0.85rem",
lineHeight: 1.5,
height: "100%",
boxSizing: "border-box",
},
},
}}
sx={{
flex: "1 1 0",
display: "flex",
flexDirection: "column",
height: 0,
minHeight: 0,
"& .MuiInputBase-root": {
flex: "1 1 0",
display: "flex",
flexDirection: "column",
alignItems: "stretch",
height: "100%",
minHeight: 0,
},
"& .MuiInputBase-input": {
flexGrow: 1,
overflow: "auto !important",
height: "100% !important",
resize: "none",
padding: 0,
},
}}
/>
</Box>
</Paper>
</Grid>
</Grid>
</Box>
);
}
export default MainPage;

View File

@@ -1,11 +1,11 @@
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #f8f9fa;
overflow: hidden;
}
code {
@@ -13,12 +13,6 @@ code {
monospace;
}
.container-fluid {
height: 100vh;
display: flex;
flex-direction: column;
}
.content-section {
flex: 1;
min-height: 0;
@@ -32,8 +26,6 @@ code {
.form-control {
resize: vertical;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 14px;
}
.input-section .form-control {
@@ -48,48 +40,61 @@ code {
}
.error {
background-color: #f8d7da !important;
border-color: #f5c6cb !important;
color: #721c24;
background-color: var(--error-bg-light) !important;
border-color: var(--error-border-light) !important;
color: var(--error-text-light);
}
.success {
background-color: #d4edda !important;
border-color: #c3e6cb !important;
color: #155724;
background-color: var(--success-bg-light) !important;
border-color: var(--success-border-light) !important;
color: var(--success-text-light);
}
.btn-outline-success:hover {
background-color: #28a745;
border-color: #28a745;
color: white;
/* Dark mode support for error states */
@media (prefers-color-scheme: dark) {
.error {
background-color: var(--error-bg-dark) !important;
border-color: var(--error-border-dark) !important;
color: var(--error-text-dark) !important;
}
}
.btn-outline-info:hover {
background-color: #17a2b8;
border-color: #17a2b8;
color: white;
/* Manual theme overrides for error states */
.theme-dark .error {
background-color: var(--error-bg-dark) !important;
border-color: var(--error-border-dark) !important;
color: var(--error-text-dark) !important;
}
.header-section {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 2rem 0;
margin-bottom: 2rem;
.theme-light .error {
background-color: var(--error-bg-light) !important;
border-color: var(--error-border-light) !important;
color: var(--error-text-light) !important;
}
.jmespath-input {
background-color: #fff3cd;
border-color: #ffeaa7;
font-weight: 500;
/* Manual theme overrides for success states */
.theme-dark .success {
background-color: var(--success-bg-dark) !important;
border-color: var(--success-border-dark) !important;
color: var(--success-text-dark) !important;
}
.json-input {
background-color: #e8f5e8;
border-color: #c3e6cb;
.theme-light .success {
background-color: var(--success-bg-light) !important;
border-color: var(--success-border-light) !important;
color: var(--success-text-light) !important;
}
.result-output {
background-color: #e7f3ff;
border-color: #b3d7ff;
/* Additional specificity for jmespath-input with error class */
.theme-dark .jmespath-input.error {
background-color: #4a1e1e !important;
border-color: #6d2c2c !important;
color: #f8d7da !important;
}
.theme-light .jmespath-input.error {
background-color: #f8d7da !important;
border-color: #f5c6cb !important;
color: #721c24 !important;
}

View File

@@ -1,18 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import 'bootstrap/dist/css/bootstrap.min.css';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

15
src/index.jsx Normal file
View File

@@ -0,0 +1,15 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { ThemeProvider } from "@mui/material";
import theme from "./theme";
import "./index.css";
import App from "./App";
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<ThemeProvider theme={theme} defaultMode="system">
<App />
</ThemeProvider>
</React.StrictMode>
);

View File

@@ -1,13 +0,0 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

View File

@@ -1,5 +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';

13
src/theme.js Normal file
View File

@@ -0,0 +1,13 @@
import { createTheme } from "@mui/material";
const theme = createTheme({
cssVariables: {
colorSchemeSelector: 'class',
},
colorSchemes: {
light: true,
dark: true,
},
});
export default theme;

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,
},
});