Fix: the expression input box was getting reset while switching pages. Formatted the code text.

This commit is contained in:
2026-01-31 09:05:07 +01:00
parent 72d1be0bdc
commit b7df3e731f
2 changed files with 140 additions and 120 deletions

View File

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

View File

@@ -1,41 +1,23 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from "react";
import jmespath from 'jmespath'; import jmespath from "jmespath";
function MainPage({ apiKey, showReloadButton, onReloadSampleData, initialSampleData }) {
const [jmespathExpression, setJmespathExpression] = useState('people[0].name');
const [jsonData, setJsonData] = useState(`{
"people": [
{
"name": "John Doe",
"age": 30,
"city": "New York"
},
{
"name": "Jane Smith",
"age": 25,
"city": "Los Angeles"
}
],
"total": 2
}`);
const [result, setResult] = useState('');
const [error, setError] = useState('');
const [jsonError, setJsonError] = useState('');
// Use initial sample data when provided
useEffect(() => {
if (initialSampleData) {
setJsonData(JSON.stringify(initialSampleData, null, 2));
}
}, [initialSampleData]);
function MainPage({
showReloadButton,
onReloadSampleData,
jmespathExpression,
setJmespathExpression,
jsonData,
setJsonData,
}) {
const [result, setResult] = useState("");
const [error, setError] = useState("");
const [jsonError, setJsonError] = useState("");
const evaluateExpression = () => { const evaluateExpression = () => {
try { try {
// Clear previous errors // Clear previous errors
setError(''); setError("");
setJsonError(''); setJsonError("");
// Validate and parse JSON // Validate and parse JSON
let parsedData; let parsedData;
@@ -43,7 +25,7 @@ function MainPage({ apiKey, showReloadButton, onReloadSampleData, initialSampleD
parsedData = JSON.parse(jsonData); parsedData = JSON.parse(jsonData);
} catch (jsonErr) { } catch (jsonErr) {
setJsonError(`Invalid JSON: ${jsonErr.message}`); setJsonError(`Invalid JSON: ${jsonErr.message}`);
setResult(''); setResult("");
return; return;
} }
@@ -52,13 +34,13 @@ function MainPage({ apiKey, showReloadButton, onReloadSampleData, initialSampleD
// Format the result // Format the result
if (queryResult === null || queryResult === undefined) { if (queryResult === null || queryResult === undefined) {
setResult('null'); setResult("null");
} else { } else {
setResult(JSON.stringify(queryResult, null, 2)); setResult(JSON.stringify(queryResult, null, 2));
} }
} catch (jmesErr) { } catch (jmesErr) {
setError(`JMESPath Error: ${jmesErr.message}`); setError(`JMESPath Error: ${jmesErr.message}`);
setResult(''); setResult("");
} }
}; };
@@ -88,30 +70,30 @@ function MainPage({ apiKey, showReloadButton, onReloadSampleData, initialSampleD
}; };
const clearAll = () => { const clearAll = () => {
setJmespathExpression(''); setJmespathExpression("");
setJsonData(''); setJsonData("");
setResult(''); setResult("");
setError(''); setError("");
setJsonError(''); setJsonError("");
}; };
const loadSample = () => { const loadSample = () => {
const sampleData = { const sampleData = {
"users": [ users: [
{"name": "Alice", "age": 30, "city": "New York"}, { name: "Alice", age: 30, city: "New York" },
{"name": "Bob", "age": 25, "city": "San Francisco"}, { name: "Bob", age: 25, city: "San Francisco" },
{"name": "Charlie", "age": 35, "city": "Chicago"} { name: "Charlie", age: 35, city: "Chicago" },
], ],
"total": 3 total: 3,
}; };
setJsonData(JSON.stringify(sampleData, null, 2)); setJsonData(JSON.stringify(sampleData, null, 2));
setJmespathExpression('users[?age > `30`].name'); setJmespathExpression("users[?age > `30`].name");
}; };
const loadFromDisk = () => { const loadFromDisk = () => {
const input = document.createElement('input'); const input = document.createElement("input");
input.type = 'file'; input.type = "file";
input.accept = '.json'; input.accept = ".json";
input.onchange = (e) => { input.onchange = (e) => {
const file = e.target.files[0]; const file = e.target.files[0];
if (file) { if (file) {
@@ -122,7 +104,7 @@ function MainPage({ apiKey, showReloadButton, onReloadSampleData, initialSampleD
const parsed = JSON.parse(content); const parsed = JSON.parse(content);
setJsonData(JSON.stringify(parsed, null, 2)); setJsonData(JSON.stringify(parsed, null, 2));
} catch (error) { } catch (error) {
alert('Invalid JSON file'); alert("Invalid JSON file");
} }
}; };
reader.readAsText(file); reader.readAsText(file);
@@ -132,9 +114,9 @@ function MainPage({ apiKey, showReloadButton, onReloadSampleData, initialSampleD
}; };
const loadLogFile = () => { const loadLogFile = () => {
const input = document.createElement('input'); const input = document.createElement("input");
input.type = 'file'; input.type = "file";
input.accept = '.log,.jsonl,.ndjson'; input.accept = ".log,.jsonl,.ndjson";
input.onchange = (e) => { input.onchange = (e) => {
const file = e.target.files[0]; const file = e.target.files[0];
if (file) { if (file) {
@@ -142,12 +124,12 @@ function MainPage({ apiKey, showReloadButton, onReloadSampleData, initialSampleD
reader.onload = (e) => { reader.onload = (e) => {
try { try {
const content = e.target.result; const content = e.target.result;
const lines = content.trim().split('\n'); const lines = content.trim().split("\n");
const logs = lines.map(line => JSON.parse(line)); const logs = lines.map((line) => JSON.parse(line));
setJsonData(JSON.stringify(logs, null, 2)); setJsonData(JSON.stringify(logs, null, 2));
setJmespathExpression('[*].message'); setJmespathExpression("[*].message");
} catch (error) { } catch (error) {
alert('Invalid JSON Lines file'); alert("Invalid JSON Lines file");
} }
}; };
reader.readAsText(file); reader.readAsText(file);
@@ -162,8 +144,9 @@ function MainPage({ apiKey, showReloadButton, onReloadSampleData, initialSampleD
<div className="row mb-2"> <div className="row mb-2">
<div className="col-12"> <div className="col-12">
<p className="text-muted text-center mb-2 small"> <p className="text-muted text-center mb-2 small">
Validate and test JMESPath expressions against JSON data in real-time. Validate and test JMESPath expressions against JSON data in
Enter your JMESPath query and JSON data below to see the results instantly. real-time. Enter your JMESPath query and JSON data below to see the
results instantly.
</p> </p>
</div> </div>
</div> </div>
@@ -218,13 +201,17 @@ function MainPage({ apiKey, showReloadButton, onReloadSampleData, initialSampleD
<div className="card-body"> <div className="card-body">
<input <input
type="text" type="text"
className={`form-control jmespath-input ${error ? 'error' : 'success'}`} className={`form-control jmespath-input ${error ? "error" : "success"}`}
value={jmespathExpression} value={jmespathExpression}
onChange={handleJmespathChange} onChange={handleJmespathChange}
placeholder="Enter JMESPath expression (e.g., people[*].name)" placeholder="Enter JMESPath expression (e.g., people[*].name)"
/> />
<div className={`alert mt-2 mb-0 d-flex justify-content-between align-items-center ${error ? 'alert-danger' : 'alert-success'}`}> <div
<small className="mb-0">{error || 'Expression is correct'}</small> className={`alert mt-2 mb-0 d-flex justify-content-between align-items-center ${error ? "alert-danger" : "alert-success"}`}
>
<small className="mb-0">
{error || "Expression is correct"}
</small>
{showReloadButton && ( {showReloadButton && (
<button <button
className="btn btn-light btn-sm ms-2 border" className="btn btn-light btn-sm ms-2 border"
@@ -254,13 +241,16 @@ function MainPage({ apiKey, showReloadButton, onReloadSampleData, initialSampleD
JSON Data JSON Data
</h6> </h6>
</div> </div>
<div className="card-body flex-grow-1 d-flex flex-column" style={{ minHeight: 0 }}> <div
className="card-body flex-grow-1 d-flex flex-column"
style={{ minHeight: 0 }}
>
<textarea <textarea
className={`form-control json-input flex-grow-1 ${jsonError ? 'error' : 'success'}`} className={`form-control json-input flex-grow-1 ${jsonError ? "error" : "success"}`}
value={jsonData} value={jsonData}
onChange={handleJsonChange} onChange={handleJsonChange}
placeholder="Enter JSON data here..." placeholder="Enter JSON data here..."
style={{ minHeight: 0, resize: 'none' }} style={{ minHeight: 0, resize: "none" }}
/> />
{jsonError && ( {jsonError && (
<div className="alert alert-danger mt-2 mb-0"> <div className="alert alert-danger mt-2 mb-0">
@@ -280,13 +270,16 @@ function MainPage({ apiKey, showReloadButton, onReloadSampleData, initialSampleD
Results Results
</h6> </h6>
</div> </div>
<div className="card-body flex-grow-1 d-flex flex-column" style={{ minHeight: 0 }}> <div
className="card-body flex-grow-1 d-flex flex-column"
style={{ minHeight: 0 }}
>
<textarea <textarea
className="form-control result-output flex-grow-1" className="form-control result-output flex-grow-1"
value={result} value={result}
readOnly readOnly
placeholder="Results will appear here..." placeholder="Results will appear here..."
style={{ minHeight: 0, resize: 'none' }} style={{ minHeight: 0, resize: "none" }}
/> />
</div> </div>
</div> </div>