Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 44bb4b7458 | |||
| 794fd88e8d | |||
| 9f0d7ee70a | |||
| 4c964cdfeb | |||
| be6dc0de60 | |||
| dc9def4faf | |||
| 3dd352df92 | |||
| 57371feeb0 |
6
.github/copilot-instructions.md
vendored
6
.github/copilot-instructions.md
vendored
@@ -71,11 +71,9 @@ The UI generates an API key at startup then load the sample data at startup and
|
||||
|
||||
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
|
||||
|
||||
@@ -4,7 +4,7 @@ A React-based web application for testing and validating JMESPath expressions ag
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
## Features
|
||||
|
||||
@@ -127,7 +127,7 @@ MAX_SESSIONS=200 MAX_SAMPLE_SIZE=2097152 LISTEN_PORT=8080 node server.js
|
||||
## Technology Stack
|
||||
|
||||
- **React 18.2.0**: Frontend framework with modern hooks and components
|
||||
- **Bootstrap 5.3.2**: CSS framework with dark/light theme support
|
||||
- **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
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
#!/usr/bin/env pwsh
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Position=0, HelpMessage='API base URL')]
|
||||
[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='Path to JSON file; default: read from stdin')]
|
||||
[string]$JsonFile = '-',
|
||||
|
||||
[Parameter(HelpMessage='Show help')]
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
763
package-lock.json
generated
763
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
11
package.json
11
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "jmespath-playground",
|
||||
"version": "1.3.0",
|
||||
"version": "1.4.0",
|
||||
"description": "A React-based web application for testing JMESPath expressions against JSON data",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
@@ -9,18 +9,21 @@
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest",
|
||||
"server": "node server.js",
|
||||
"dev": "concurrently \"npm start\" \"npm run server\"",
|
||||
"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",
|
||||
|
||||
@@ -195,11 +195,7 @@ function createApp(devMode = false) {
|
||||
`📁 Session created: ${sessionId.substring(0, 8)}... (${sessions.size}/${MAX_SESSIONS})`,
|
||||
);
|
||||
|
||||
res.json({
|
||||
message: "Sample data uploaded successfully",
|
||||
state: stateGuid,
|
||||
sessionId: sessionId.substring(0, 8) + "...",
|
||||
});
|
||||
res.json({ message: "OK" });
|
||||
} catch (error) {
|
||||
console.error("⚠️ Upload endpoint exception occurred:", {
|
||||
message: error.message,
|
||||
|
||||
262
src/App.css
262
src/App.css
@@ -1,262 +1,46 @@
|
||||
/* JMESPath Testing Tool Custom Styles */
|
||||
/* JMESPath Testing Tool - Minimal Styles */
|
||||
|
||||
:root {
|
||||
/* Common variables */
|
||||
--font-mono: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
|
||||
--accent-color: #007bff;
|
||||
|
||||
/* Button variants */
|
||||
--btn-success: #28a745;
|
||||
--btn-info: #17a2b8;
|
||||
--btn-primary: #007bff;
|
||||
--btn-danger: #dc3545;
|
||||
--btn-secondary: #6c757d;
|
||||
|
||||
/* Common transitions */
|
||||
--transition-fast: 0.2s ease;
|
||||
--transition-normal: 0.3s ease;
|
||||
|
||||
/* Font families */
|
||||
--font-sans: 'Noto Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
|
||||
--font-mono: 'Noto Sans Mono', 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
--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: var(--font-sans);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
transition: background-color var(--transition-normal), color var(--transition-normal);
|
||||
}
|
||||
|
||||
/* Layout structure */
|
||||
.vh-100 {
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
/* Header section styling - more compact */
|
||||
.header-section {
|
||||
/* Removed gradient background to fix text visibility */
|
||||
margin: 0;
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
/* Custom card styling */
|
||||
.card {
|
||||
border: none;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
||||
border-radius: 8px;
|
||||
transition: background-color 0.3s ease, box-shadow 0.3s ease;
|
||||
#root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 2px solid #dee2e6;
|
||||
font-weight: 600;
|
||||
color: #212529;
|
||||
transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease;
|
||||
/* Scrollbar styling for a cleaner look */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
/* Input and textarea styling */
|
||||
.jmespath-input, .json-input, .result-output {
|
||||
font-family: var(--font-mono);
|
||||
font-weight: 400;
|
||||
transition: background-color var(--transition-normal), border-color var(--transition-normal), color var(--transition-normal);
|
||||
::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.jmespath-input {
|
||||
font-size: 14px;
|
||||
padding: 10px;
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.json-input, .result-output {
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
[data-mui-color-scheme="dark"] ::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
/* Button styling */
|
||||
.btn {
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* Footer styling */
|
||||
footer {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
}
|
||||
|
||||
/* Bootstrap theme integration */
|
||||
[data-bs-theme="light"] {
|
||||
--bg-primary: #ffffff;
|
||||
--bg-secondary: #f8f9fa;
|
||||
--text-primary: #212529;
|
||||
--text-secondary: #495057;
|
||||
--text-muted: #6c757d;
|
||||
--border: #dee2e6;
|
||||
--border-input: #ced4da;
|
||||
|
||||
--success-bg: #d4edda;
|
||||
--success-border: #c3e6cb;
|
||||
--success-text: #155724;
|
||||
|
||||
--error-bg: #f8d7da;
|
||||
--error-border: #f5c6cb;
|
||||
--error-text: #721c24;
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] {
|
||||
--bg-primary: #1a1a1a;
|
||||
--bg-secondary: #2d2d2d;
|
||||
--bg-card: #323232;
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #e9ecef;
|
||||
--text-muted: #adb5bd;
|
||||
--border: #495057;
|
||||
--border-input: #6c757d;
|
||||
|
||||
--success-bg: #1e4a1e;
|
||||
--success-border: #2c6d2c;
|
||||
--success-text: #d4edda;
|
||||
|
||||
--error-bg: #4a1e1e;
|
||||
--error-border: #6d2c2c;
|
||||
--error-text: #f8d7da;
|
||||
}
|
||||
|
||||
/* Apply theme colors */
|
||||
body {
|
||||
background-color: var(--bg-primary);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: var(--bg-primary);
|
||||
border-color: var(--border);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.card-header {
|
||||
background-color: var(--bg-secondary);
|
||||
border-bottom-color: var(--border);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.jmespath-input {
|
||||
background-color: var(--bg-primary);
|
||||
border-color: var(--border-input);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.json-input, .result-output {
|
||||
background-color: var(--bg-secondary);
|
||||
border-color: var(--border);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
footer {
|
||||
background-color: var(--bg-secondary);
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
footer.bg-light {
|
||||
background-color: var(--bg-secondary) !important;
|
||||
}
|
||||
|
||||
footer a {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
footer a:hover {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
/* State styles */
|
||||
.jmespath-input.success {
|
||||
background-color: var(--success-bg) !important;
|
||||
border-color: var(--success-border) !important;
|
||||
color: var(--success-text) !important;
|
||||
}
|
||||
|
||||
.jmespath-input.error {
|
||||
background-color: var(--error-bg) !important;
|
||||
border-color: var(--error-border) !important;
|
||||
color: var(--error-text) !important;
|
||||
}
|
||||
|
||||
.json-input.success {
|
||||
background-color: var(--success-bg) !important;
|
||||
border-color: var(--success-border) !important;
|
||||
color: var(--success-text) !important;
|
||||
}
|
||||
|
||||
.json-input.error {
|
||||
background-color: var(--error-bg) !important;
|
||||
border-color: var(--error-border) !important;
|
||||
color: var(--error-text) !important;
|
||||
}
|
||||
|
||||
/* Focus states */
|
||||
.jmespath-input:focus {
|
||||
border-color: var(--accent-color, #007bff);
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
.json-input:focus,
|
||||
.result-output:focus {
|
||||
background-color: var(--bg-primary);
|
||||
border-color: var(--accent-color, #007bff);
|
||||
color: var(--text-secondary);
|
||||
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
|
||||
}
|
||||
|
||||
/* Placeholder colors */
|
||||
.jmespath-input::placeholder,
|
||||
.json-input::placeholder,
|
||||
.result-output::placeholder {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* Alert styles */
|
||||
.alert-danger {
|
||||
background-color: var(--error-bg);
|
||||
border-color: var(--error-border);
|
||||
color: var(--error-text);
|
||||
}
|
||||
|
||||
/* Code block styles */
|
||||
pre.bg-light {
|
||||
background-color: var(--bg-secondary) !important;
|
||||
color: var(--text-secondary) !important;
|
||||
border-color: var(--border) !important;
|
||||
}
|
||||
|
||||
code {
|
||||
color: var(--text-secondary);
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
|
||||
88
src/App.jsx
88
src/App.jsx
@@ -1,4 +1,9 @@
|
||||
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";
|
||||
@@ -31,6 +36,10 @@ function App() {
|
||||
// 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] =
|
||||
@@ -61,36 +70,19 @@ function App() {
|
||||
return newKey;
|
||||
});
|
||||
|
||||
// Theme management
|
||||
useEffect(() => {
|
||||
const applyTheme = (selectedTheme) => {
|
||||
const effectiveTheme =
|
||||
selectedTheme === "auto"
|
||||
? window.matchMedia &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches
|
||||
? "dark"
|
||||
: "light"
|
||||
: selectedTheme;
|
||||
const getApiHeaders = () => ({
|
||||
"Accept": "application/json",
|
||||
"x-api-key": apiKey,
|
||||
});
|
||||
|
||||
document.documentElement.setAttribute("data-bs-theme", effectiveTheme);
|
||||
};
|
||||
|
||||
applyTheme(theme);
|
||||
|
||||
// Save theme preference
|
||||
localStorage.setItem("theme", theme);
|
||||
}, [theme]);
|
||||
|
||||
// Get headers for API requests
|
||||
const getApiHeaders = () => {
|
||||
return {
|
||||
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
|
||||
@@ -156,14 +148,32 @@ function App() {
|
||||
|
||||
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 (
|
||||
<div className="container-fluid vh-100 d-flex flex-column">
|
||||
<>
|
||||
<CssBaseline />
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
height: "100vh",
|
||||
overflow: "hidden",
|
||||
bgcolor: "background.default",
|
||||
}}
|
||||
>
|
||||
<Header
|
||||
theme={theme}
|
||||
onThemeChange={handleThemeChange}
|
||||
@@ -172,9 +182,15 @@ function App() {
|
||||
/>
|
||||
|
||||
{/* Main Content Section - flex-grow to fill space */}
|
||||
<div
|
||||
className="container-fluid flex-grow-1 d-flex flex-column"
|
||||
style={{ minHeight: 0 }}
|
||||
<Box
|
||||
component="main"
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
minHeight: 0,
|
||||
height: "100%", // Force height for children
|
||||
}}
|
||||
>
|
||||
{currentPage === "main" ? (
|
||||
<MainPage
|
||||
@@ -187,12 +203,18 @@ function App() {
|
||||
setJsonData={setJsonData}
|
||||
/>
|
||||
) : (
|
||||
<ApiKeyPage apiKey={apiKey} onRegenerateApiKey={regenerateApiKey} />
|
||||
<ApiKeyPage
|
||||
apiKey={apiKey}
|
||||
onRegenerateApiKey={regenerateApiKey}
|
||||
shellType={shellType}
|
||||
onShellTypeChange={handleShellTypeChange}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Box>
|
||||
|
||||
<Footer />
|
||||
</div>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ describe('App Component', () => {
|
||||
describe('Basic Rendering', () => {
|
||||
test('renders JMESPath Testing Tool title', () => {
|
||||
render(<App />);
|
||||
const titleElement = screen.getByRole('heading', { name: /JMESPath Testing Tool/i });
|
||||
const titleElement = screen.getByText(/JMESPath Playground/i);
|
||||
expect(titleElement).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -90,11 +90,11 @@ describe('App Component', () => {
|
||||
|
||||
test('renders all toolbar buttons', () => {
|
||||
render(<App />);
|
||||
expect(screen.getByTitle('Load JSON object from file')).toBeInTheDocument();
|
||||
expect(screen.getByTitle('Load JSON Lines log file')).toBeInTheDocument();
|
||||
expect(screen.getByTitle('Load sample data')).toBeInTheDocument();
|
||||
expect(screen.getByTitle('Format JSON')).toBeInTheDocument();
|
||||
expect(screen.getByTitle('Clear all inputs')).toBeInTheDocument();
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -108,7 +108,7 @@ describe('App Component', () => {
|
||||
const resultArea = screen.getByPlaceholderText(/Results will appear here/i);
|
||||
|
||||
// Clear all inputs first to start fresh
|
||||
const clearButton = screen.getByTitle('Clear all inputs');
|
||||
const clearButton = screen.getByRole('button', { name: /Clear all inputs/i });
|
||||
await user.click(clearButton);
|
||||
|
||||
// Set JSON data directly after clearing
|
||||
@@ -153,7 +153,7 @@ describe('App Component', () => {
|
||||
const jsonInput = screen.getByPlaceholderText(/Enter JSON data here/i);
|
||||
|
||||
// Clear all inputs first
|
||||
const clearButton = screen.getByTitle('Clear all inputs');
|
||||
const clearButton = screen.getByRole('button', { name: /Clear all inputs/i });
|
||||
await user.click(clearButton);
|
||||
|
||||
// Set invalid JSON directly
|
||||
@@ -187,31 +187,55 @@ describe('App Component', () => {
|
||||
test('renders theme switcher buttons', () => {
|
||||
render(<App />);
|
||||
|
||||
expect(screen.getByTitle('Auto (follow system)')).toBeInTheDocument();
|
||||
expect(screen.getByTitle('Light theme')).toBeInTheDocument();
|
||||
expect(screen.getByTitle('Dark theme')).toBeInTheDocument();
|
||||
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.getByTitle('Light theme');
|
||||
const lightButton = screen.getByRole('button', { name: /Light/i });
|
||||
await user.click(lightButton);
|
||||
|
||||
// Check if button becomes active
|
||||
expect(lightButton).toHaveClass('btn-primary');
|
||||
expect(lightButton).toHaveClass('Mui-selected');
|
||||
});
|
||||
|
||||
test('switches to dark theme when clicked', async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<App />);
|
||||
|
||||
const darkButton = screen.getByTitle('Dark theme');
|
||||
const darkButton = screen.getByRole('button', { name: /Dark/i });
|
||||
await user.click(darkButton);
|
||||
|
||||
// Check if button becomes active
|
||||
expect(darkButton).toHaveClass('btn-primary');
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -222,7 +246,7 @@ describe('App Component', () => {
|
||||
|
||||
const jmespathInput = screen.getByPlaceholderText(/Enter JMESPath expression/i);
|
||||
const jsonInput = screen.getByPlaceholderText(/Enter JSON data here/i);
|
||||
const clearButton = screen.getByTitle('Clear all inputs');
|
||||
const clearButton = screen.getByRole('button', { name: /Clear all inputs/i });
|
||||
|
||||
// Add some content
|
||||
await user.type(jmespathInput, 'test.expression');
|
||||
@@ -241,7 +265,7 @@ describe('App Component', () => {
|
||||
render(<App />);
|
||||
|
||||
const jsonInput = screen.getByPlaceholderText(/Enter JSON data here/i);
|
||||
const formatButton = screen.getByTitle('Format JSON');
|
||||
const formatButton = screen.getByRole('button', { name: "Format" });
|
||||
|
||||
// Add minified JSON directly
|
||||
fireEvent.change(jsonInput, { target: { value: '{"name":"Alice","age":30,"skills":["React","Node"]}' } });
|
||||
@@ -260,7 +284,7 @@ describe('App Component', () => {
|
||||
const user = userEvent.setup();
|
||||
render(<App />);
|
||||
|
||||
const loadSampleButton = screen.getByTitle('Load sample data');
|
||||
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);
|
||||
|
||||
@@ -331,7 +355,7 @@ describe('App Component', () => {
|
||||
const user = userEvent.setup();
|
||||
render(<App />);
|
||||
|
||||
const loadObjectButton = screen.getByTitle('Load JSON object from file');
|
||||
const loadObjectButton = screen.getByRole('button', { name: "Load from Disk" });
|
||||
|
||||
// Create a mock file
|
||||
const file = new File(['{"test": "file data"}'], 'test.json', {
|
||||
|
||||
@@ -1,6 +1,83 @@
|
||||
import React, { useState } from 'react';
|
||||
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 ApiKeyPage({ apiKey, onRegenerateApiKey }) {
|
||||
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 () => {
|
||||
@@ -9,85 +86,119 @@ function ApiKeyPage({ apiKey, onRegenerateApiKey }) {
|
||||
setCopySuccess(true);
|
||||
setTimeout(() => setCopySuccess(false), 2000);
|
||||
} catch (err) {
|
||||
console.error('Failed to copy to clipboard:', err);
|
||||
// Fallback for older browsers
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = apiKey;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
setCopySuccess(true);
|
||||
setTimeout(() => setCopySuccess(false), 2000);
|
||||
console.error("Failed to copy to clipboard:", err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="row justify-content-center">
|
||||
<div className="col-md-8">
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<h5 className="mb-0">🔐 API Key Management</h5>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<div className="mb-4">
|
||||
<label className="form-label fw-bold">Your API Key:</label>
|
||||
<div className="input-group">
|
||||
<input
|
||||
type="text"
|
||||
className="form-control font-monospace"
|
||||
value={apiKey}
|
||||
readOnly
|
||||
/>
|
||||
<button
|
||||
className={`btn ${copySuccess ? 'btn-success' : 'btn-outline-secondary'}`}
|
||||
onClick={handleCopyToClipboard}
|
||||
title="Copy API key to clipboard"
|
||||
>
|
||||
{copySuccess ? '✓ Copied!' : '📋 Copy'}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-primary"
|
||||
onClick={onRegenerateApiKey}
|
||||
title="Generate new API key"
|
||||
>
|
||||
🔄 Regenerate
|
||||
</button>
|
||||
</div>
|
||||
<div className="form-text">
|
||||
This API key is used to encrypt and authenticate data uploads.
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
|
||||
<div className="mb-4">
|
||||
<h6>📡 Remote Data Upload API</h6>
|
||||
<p className="text-muted">
|
||||
External tools can upload sample data remotely using the REST API.
|
||||
The API key is required for authentication. Define two
|
||||
environment variables in your <code>.bashrc</code>.
|
||||
</p>
|
||||
<pre className="bg-light p-3 rounded border">
|
||||
<code>export JMESPATH_PLAYGROUND_API_URL={window.location.origin}<br/>export JMESPATH_PLAYGROUND_API_KEY={apiKey}</code>
|
||||
</pre>
|
||||
<p className="text-muted">Then, use the following <code>curl</code> command to upload your data:</p>
|
||||
<pre className="bg-light p-3 rounded border">
|
||||
<code>{`curl -s -X POST \\
|
||||
-H "Content-Type: application/json" \\
|
||||
<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" \\
|
||||
--data @__JSON_FILE_NAME__ \\
|
||||
"$\{JMESPATH_PLAYGROUND_API_URL}/api/v1/upload"`}</code>
|
||||
</pre>
|
||||
<div className="form-text">
|
||||
Replace <code>{"__JSON_FILE_NAME__"}</code> with the path to your
|
||||
JSON file containing the sample data. or use <code>-</code> to
|
||||
read from standard input.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
-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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,27 +1,57 @@
|
||||
import React from 'react';
|
||||
import { VERSION } from '../version';
|
||||
import React from "react";
|
||||
import { Box, Typography, Container, Link, Grid } from "@mui/material";
|
||||
import { VERSION } from "../version";
|
||||
|
||||
function Footer() {
|
||||
return (
|
||||
<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> {VERSION === 'unknown' ? VERSION : `v${VERSION}`} - 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">
|
||||
<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
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</Link>
|
||||
</Typography>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,71 +1,88 @@
|
||||
import React from 'react';
|
||||
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 (
|
||||
<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>
|
||||
{/* Right side controls - better positioning */}
|
||||
<div className="position-absolute top-0 end-0 d-flex align-items-center gap-2">
|
||||
{/* API Key Management Button - more prominent */}
|
||||
<button
|
||||
type="button"
|
||||
className={`btn btn-sm ${
|
||||
currentPage === 'apikey'
|
||||
? 'btn-warning fw-bold'
|
||||
: 'btn-outline-warning'
|
||||
}`}
|
||||
onClick={() => onPageChange(currentPage === 'main' ? 'apikey' : 'main')}
|
||||
title="API Key Management"
|
||||
<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",
|
||||
}}
|
||||
>
|
||||
🔐 API Keys
|
||||
</button>
|
||||
{/* Theme switcher with theme-aware classes */}
|
||||
<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={() => onThemeChange('auto')}
|
||||
title="Auto (follow system)"
|
||||
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")}
|
||||
>
|
||||
🌓 Auto
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn ${
|
||||
theme === 'light'
|
||||
? 'btn-primary'
|
||||
: 'btn-outline-secondary'
|
||||
}`}
|
||||
onClick={() => onThemeChange('light')}
|
||||
title="Light theme"
|
||||
{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"
|
||||
>
|
||||
☀️ Light
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`btn ${
|
||||
theme === 'dark'
|
||||
? 'btn-primary'
|
||||
: 'btn-outline-secondary'
|
||||
}`}
|
||||
onClick={() => onThemeChange('dark')}
|
||||
title="Dark theme"
|
||||
>
|
||||
🌙 Dark
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,31 @@
|
||||
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({
|
||||
@@ -160,173 +187,348 @@ function MainPage({
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* 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>
|
||||
<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>
|
||||
|
||||
{/* Middle Section: JMESPath Expression Input */}
|
||||
<div className="row mb-2">
|
||||
<div className="col-12">
|
||||
<div className="card">
|
||||
<div className="card-header py-2">
|
||||
<h6 className="mb-0">
|
||||
<i className="bi bi-search me-2"></i>
|
||||
<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
|
||||
</h6>
|
||||
</div>
|
||||
<div className="card-body">
|
||||
<input
|
||||
type="text"
|
||||
className={`form-control jmespath-input ${error ? "error" : "success"}`}
|
||||
</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}
|
||||
placeholder="Enter JMESPath expression (e.g., people[*].name)"
|
||||
/>
|
||||
<div
|
||||
className={`alert mt-2 mb-0 d-flex justify-content-between align-items-center ${error ? "alert-danger" : "alert-success"}`}
|
||||
>
|
||||
<small className="mb-0">
|
||||
{error || "Expression is correct"}
|
||||
</small>
|
||||
{showReloadButton && (
|
||||
<button
|
||||
className="btn btn-light btn-sm ms-2 border"
|
||||
onClick={() => {
|
||||
onReloadSampleData();
|
||||
error={!!error}
|
||||
helperText={error || " "}
|
||||
sx={{
|
||||
"& .MuiInputBase-root": {
|
||||
fontFamily: "'Noto Sans Mono', monospace",
|
||||
fontSize: "0.9rem",
|
||||
},
|
||||
"& .MuiFormHelperText-root": {
|
||||
mt: 0.75,
|
||||
mb: -0.5,
|
||||
},
|
||||
}}
|
||||
title="New sample data is available"
|
||||
>
|
||||
<i className="bi bi-arrow-clockwise me-1"></i>
|
||||
Reload Sample Data
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
/>
|
||||
</Box>
|
||||
</Paper>
|
||||
|
||||
{/* Lower Middle Section: Input and Output Areas */}
|
||||
<div className="row flex-grow-1" style={{ minHeight: 0 }}>
|
||||
{/* Left Panel: JSON Data Input */}
|
||||
<div className="col-md-6">
|
||||
<div className="card h-100 d-flex flex-column">
|
||||
<div className="card-header d-flex justify-content-between align-items-center py-2">
|
||||
<h6 className="mb-0">
|
||||
<i className="bi bi-file-earmark-code me-2"></i>
|
||||
JSON Data
|
||||
</h6>
|
||||
<div>
|
||||
<button
|
||||
className="btn btn-outline-success btn-sm me-2"
|
||||
<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}
|
||||
title="Load JSON object from file"
|
||||
color="primary"
|
||||
aria-label="Load from Disk"
|
||||
>
|
||||
📄 Load an Object
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-info btn-sm me-2"
|
||||
<FileOpenIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Load Logs">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={loadLogFile}
|
||||
title="Load JSON Lines log file"
|
||||
color="primary"
|
||||
aria-label="Load Logs"
|
||||
>
|
||||
📋 Load a Log File
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-primary btn-sm me-2"
|
||||
<UploadFileIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Load Sample">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={loadSample}
|
||||
title="Load sample data"
|
||||
color="primary"
|
||||
aria-label="Load Sample"
|
||||
>
|
||||
Load Sample
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-secondary btn-sm me-2"
|
||||
<RestoreIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Format">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={formatJson}
|
||||
title="Format JSON"
|
||||
color="primary"
|
||||
aria-label="Format"
|
||||
>
|
||||
Format JSON
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-danger btn-sm"
|
||||
<FormatAlignLeftIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Divider orientation="vertical" flexItem sx={{ mx: 0.5 }} />
|
||||
<Tooltip title="Clear all inputs">
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={clearAll}
|
||||
title="Clear all inputs"
|
||||
color="secondary"
|
||||
aria-label="Clear all inputs"
|
||||
>
|
||||
Clear All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="card-body flex-grow-1 d-flex flex-column"
|
||||
style={{ minHeight: 0 }}
|
||||
>
|
||||
<textarea
|
||||
className={`form-control json-input flex-grow-1 ${jsonError ? "error" : "success"}`}
|
||||
<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..."
|
||||
style={{ minHeight: 0, resize: "none" }}
|
||||
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 && (
|
||||
<div className="alert alert-danger mt-2 mb-0">
|
||||
<small>{jsonError}</small>
|
||||
</div>
|
||||
<Alert severity="error" sx={{ mt: 1, flexShrink: 0 }} variant="filled">
|
||||
{jsonError}
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
{/* Right Panel: Results */}
|
||||
<div className="col-md-6">
|
||||
<div className="card h-100 d-flex flex-column">
|
||||
<div className="card-header py-2 d-flex justify-content-between align-items-center">
|
||||
<h6 className="mb-0">
|
||||
<i className="bi bi-output me-2"></i>
|
||||
Results
|
||||
</h6>
|
||||
<div>
|
||||
<button
|
||||
className={`btn btn-sm me-2 ${copySuccess ? "btn-success" : "btn-outline-secondary"}`}
|
||||
<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"}
|
||||
title="Copy result to clipboard"
|
||||
color={copySuccess ? "success" : "primary"}
|
||||
>
|
||||
<i className={`bi ${copySuccess ? "bi-check-lg" : "bi-clipboard"} me-1`}></i>
|
||||
{copySuccess ? "Copied!" : "Copy"}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-secondary btn-sm"
|
||||
{copySuccess ? <CheckIcon fontSize="small" /> : <ContentCopyIcon fontSize="small" />}
|
||||
</IconButton>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip title="Download Result">
|
||||
<span>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={downloadResult}
|
||||
disabled={!result || result === "null"}
|
||||
title="Download result as JSON file"
|
||||
color="primary"
|
||||
>
|
||||
<i className="bi bi-download me-1"></i>
|
||||
Download
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="card-body flex-grow-1 d-flex flex-column"
|
||||
style={{ minHeight: 0 }}
|
||||
>
|
||||
<textarea
|
||||
className="form-control result-output flex-grow-1"
|
||||
<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}
|
||||
readOnly
|
||||
variant="standard"
|
||||
placeholder="Results will appear here..."
|
||||
style={{ minHeight: 0, resize: "none" }}
|
||||
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,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</Box>
|
||||
</Paper>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background-color: #f8f9fa;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
code {
|
||||
@@ -8,12 +13,6 @@ code {
|
||||
monospace;
|
||||
}
|
||||
|
||||
.container-fluid {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content-section {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
@@ -52,13 +51,6 @@ code {
|
||||
color: var(--success-text-light);
|
||||
}
|
||||
|
||||
.header-section {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 2rem 0;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
/* Dark mode support for error states */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.error {
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
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 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>
|
||||
);
|
||||
13
src/theme.js
Normal file
13
src/theme.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createTheme } from "@mui/material";
|
||||
|
||||
const theme = createTheme({
|
||||
cssVariables: {
|
||||
colorSchemeSelector: 'class',
|
||||
},
|
||||
colorSchemes: {
|
||||
light: true,
|
||||
dark: true,
|
||||
},
|
||||
});
|
||||
|
||||
export default theme;
|
||||
@@ -4,10 +4,12 @@ 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://localhost:3000',
|
||||
target: 'http://127.0.0.1:3000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user