Files
jmespath-playground/src/App.js
Slawomir Koszewski 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

409 lines
13 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 [theme, setTheme] = useState(() => {
// Load theme from localStorage or default to 'auto'
return localStorage.getItem('theme') || 'auto';
});
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('');
// Theme management
useEffect(() => {
// Apply theme to document
const applyTheme = (selectedTheme) => {
const root = document.documentElement;
const body = document.body;
// Clear existing theme classes from both html and body
root.className = '';
body.classList.remove('theme-light', 'theme-dark');
if (selectedTheme === 'light') {
body.classList.add('theme-light');
} else if (selectedTheme === 'dark') {
body.classList.add('theme-dark');
}
// 'auto' uses CSS media queries (no class needed)
};
applyTheme(theme);
localStorage.setItem('theme', theme);
}, [theme]);
const handleThemeChange = (newTheme) => {
setTheme(newTheme);
};
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 position-relative">
<h2 className="mb-1">JMESPath Testing Tool</h2>
{/* Theme switcher */}
<div className="position-absolute top-0 end-0">
<div className="btn-group btn-group-sm" role="group" aria-label="Theme switcher">
<button
type="button"
className={`btn ${theme === 'auto' ? 'btn-primary' : 'btn-outline-secondary'}`}
onClick={() => handleThemeChange('auto')}
title="Auto (follow system)"
>
🌓 Auto
</button>
<button
type="button"
className={`btn ${theme === 'light' ? 'btn-primary' : 'btn-outline-secondary'}`}
onClick={() => handleThemeChange('light')}
title="Light theme"
>
Light
</button>
<button
type="button"
className={`btn ${theme === 'dark' ? 'btn-primary' : 'btn-outline-secondary'}`}
onClick={() => handleThemeChange('dark')}
title="Dark theme"
>
🌙 Dark
</button>
</div>
</div>
</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' : 'success'}`}
value={jmespathExpression}
onChange={handleJmespathChange}
placeholder="Enter JMESPath expression (e.g., people[*].name)"
/>
<div className={`alert mt-2 mb-0 ${error ? 'alert-danger' : 'alert-success'}`}>
<small>{error || 'Expression is correct'}</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"
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"
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="https://opensource.org/licenses/MIT" target="_blank" rel="noopener noreferrer" 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;