Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 44bb4b7458 | |||
| 794fd88e8d | |||
| 9f0d7ee70a | |||
| 4c964cdfeb | |||
| be6dc0de60 | |||
| dc9def4faf | |||
| 3dd352df92 |
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.
|
The main components of the application are located in the `src` directory and target Node 24 LTS environment.
|
||||||
|
|
||||||
Framework to be used:
|
- Material UI v7 for building the user interface.
|
||||||
|
- React for building the component logic.
|
||||||
- React for building the user interface.
|
|
||||||
- JavaScript (ES6+) for scripting.
|
- JavaScript (ES6+) for scripting.
|
||||||
- Bootstrap for styling and layout.
|
|
||||||
- Express.js for serving the application and handling API requests.
|
- Express.js for serving the application and handling API requests.
|
||||||
|
|
||||||
### API
|
### API
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ A React-based web application for testing and validating JMESPath expressions ag
|
|||||||
|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ MAX_SESSIONS=200 MAX_SAMPLE_SIZE=2097152 LISTEN_PORT=8080 node server.js
|
|||||||
## Technology Stack
|
## Technology Stack
|
||||||
|
|
||||||
- **React 18.2.0**: Frontend framework with modern hooks and components
|
- **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
|
- **JMESPath 0.16.0**: JMESPath expression evaluation library
|
||||||
- **Express.js 4.19.2**: Backend API server with session management
|
- **Express.js 4.19.2**: Backend API server with session management
|
||||||
- **Node.js 24 LTS**: Runtime environment
|
- **Node.js 24 LTS**: Runtime environment
|
||||||
|
|||||||
763
package-lock.json
generated
763
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "jmespath-playground",
|
"name": "jmespath-playground",
|
||||||
"version": "1.3.1",
|
"version": "1.4.0",
|
||||||
"description": "A React-based web application for testing JMESPath expressions against JSON data",
|
"description": "A React-based web application for testing JMESPath expressions against JSON data",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -10,17 +10,20 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"server": "node server.js --dev",
|
"server": "node server.js --dev",
|
||||||
"dev": "concurrently \"npm start\" \"npm run server\"",
|
"dev": "concurrently \"npm start\" \"node --watch server.js --dev\"",
|
||||||
"build-image": "node scripts/build-image.js"
|
"build-image": "node scripts/build-image.js"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=24.0.0"
|
"node": ">=24.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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/jest-dom": "^6.1.4",
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/react": "^13.4.0",
|
||||||
"@testing-library/user-event": "^14.5.1",
|
"@testing-library/user-event": "^14.5.1",
|
||||||
"bootstrap": "^5.3.2",
|
|
||||||
"express": "^4.19.2",
|
"express": "^4.19.2",
|
||||||
"jmespath": "^0.16.0",
|
"jmespath": "^0.16.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
|||||||
@@ -195,11 +195,7 @@ function createApp(devMode = false) {
|
|||||||
`📁 Session created: ${sessionId.substring(0, 8)}... (${sessions.size}/${MAX_SESSIONS})`,
|
`📁 Session created: ${sessionId.substring(0, 8)}... (${sessions.size}/${MAX_SESSIONS})`,
|
||||||
);
|
);
|
||||||
|
|
||||||
res.json({
|
res.json({ message: "OK" });
|
||||||
message: "Sample data uploaded successfully",
|
|
||||||
state: stateGuid,
|
|
||||||
sessionId: sessionId.substring(0, 8) + "...",
|
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("⚠️ Upload endpoint exception occurred:", {
|
console.error("⚠️ Upload endpoint exception occurred:", {
|
||||||
message: error.message,
|
message: error.message,
|
||||||
|
|||||||
318
src/App.css
318
src/App.css
@@ -1,318 +1,46 @@
|
|||||||
/* JMESPath Testing Tool Custom Styles */
|
/* JMESPath Testing Tool - Minimal Styles */
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
/* Common variables */
|
--font-sans: "Noto Sans", -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||||
--font-mono: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
|
"Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
|
||||||
--accent-color: #007bff;
|
"Helvetica Neue", sans-serif;
|
||||||
|
--font-mono: "JetBrains Mono", "Fira Code", "Noto Sans Mono", "Consolas", "Monaco", "Courier New", monospace;
|
||||||
/* Brand colors */
|
|
||||||
--brand-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
--brand-white: #ffffff;
|
|
||||||
--brand-dark: #212529;
|
|
||||||
--brand-warning: #ffc107;
|
|
||||||
|
|
||||||
/* Brand opacity levels */
|
|
||||||
--brand-white-60: rgba(255, 255, 255, 0.6);
|
|
||||||
--brand-white-10: rgba(255, 255, 255, 0.1);
|
|
||||||
--brand-warning-50: rgba(255, 193, 7, 0.5);
|
|
||||||
--brand-warning-10: rgba(255, 193, 7, 0.1);
|
|
||||||
|
|
||||||
/* Elevation and overlays */
|
|
||||||
--shadow-light: rgba(0, 0, 0, 0.1);
|
|
||||||
--focus-ring: rgba(0, 123, 255, 0.25);
|
|
||||||
|
|
||||||
/* Button variants */
|
|
||||||
--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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Base font family */
|
|
||||||
body {
|
body {
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
transition: background-color var(--transition-normal), color var(--transition-normal);
|
margin: 0;
|
||||||
}
|
|
||||||
|
|
||||||
/* Layout structure */
|
|
||||||
.vh-100 {
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Header section styling - more compact */
|
|
||||||
.header-section {
|
|
||||||
background: var(--brand-gradient);
|
|
||||||
color: var(--brand-white);
|
|
||||||
padding: 1.2rem 0;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
transition: background-color 0.3s ease;
|
transition: background-color 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-section h2 {
|
#root {
|
||||||
color: var(--brand-white);
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure buttons in header are clearly visible against gradient */
|
/* Scrollbar styling for a cleaner look */
|
||||||
.header-section .btn-light.active {
|
::-webkit-scrollbar {
|
||||||
background-color: var(--brand-white);
|
width: 8px;
|
||||||
color: var(--brand-dark) !important; /* Deep dark text for selected states */
|
height: 8px;
|
||||||
border-color: var(--brand-white);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-section .btn-outline-light {
|
::-webkit-scrollbar-track {
|
||||||
color: var(--brand-white);
|
background: transparent;
|
||||||
border-color: var(--brand-white-60);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-section .btn-outline-light:hover {
|
::-webkit-scrollbar-thumb {
|
||||||
background-color: var(--brand-white-10);
|
background: rgba(0, 0, 0, 0.1);
|
||||||
color: var(--brand-white);
|
border-radius: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-section .btn-outline-warning {
|
[data-mui-color-scheme="dark"] ::-webkit-scrollbar-thumb {
|
||||||
color: var(--brand-warning);
|
background: rgba(255, 255, 255, 0.1);
|
||||||
border-color: var(--brand-warning-50);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-section .btn-outline-warning:hover {
|
::-webkit-scrollbar-thumb:hover {
|
||||||
background-color: var(--brand-warning-10);
|
background: rgba(0, 0, 0, 0.2);
|
||||||
color: var(--brand-warning);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Custom card styling */
|
|
||||||
.card {
|
|
||||||
border: none;
|
|
||||||
box-shadow: 0 2px 8px var(--shadow-light);
|
|
||||||
border-radius: 8px;
|
|
||||||
transition: background-color 0.3s ease, box-shadow 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.card-header {
|
|
||||||
background-color: var(--bg-secondary);
|
|
||||||
border-bottom: 2px solid var(--border);
|
|
||||||
font-weight: 600;
|
|
||||||
color: var(--text-primary);
|
|
||||||
transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.jmespath-input {
|
|
||||||
font-size: 14px;
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.json-input, .result-output {
|
|
||||||
font-size: 13px;
|
|
||||||
line-height: 1.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 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 var(--focus-ring);
|
|
||||||
}
|
|
||||||
|
|
||||||
.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 var(--focus-ring);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert-success {
|
|
||||||
background-color: var(--success-bg);
|
|
||||||
border-color: var(--success-border);
|
|
||||||
color: var(--success-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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
136
src/App.jsx
136
src/App.jsx
@@ -1,4 +1,9 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
|
import {
|
||||||
|
CssBaseline,
|
||||||
|
Box,
|
||||||
|
useColorScheme,
|
||||||
|
} from "@mui/material";
|
||||||
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";
|
||||||
@@ -65,41 +70,19 @@ function App() {
|
|||||||
return newKey;
|
return newKey;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Theme management
|
const getApiHeaders = () => ({
|
||||||
useEffect(() => {
|
"Accept": "application/json",
|
||||||
const applyTheme = (selectedTheme) => {
|
"x-api-key": apiKey,
|
||||||
const effectiveTheme =
|
});
|
||||||
selectedTheme === "auto"
|
|
||||||
? window.matchMedia &&
|
|
||||||
window.matchMedia("(prefers-color-scheme: dark)").matches
|
|
||||||
? "dark"
|
|
||||||
: "light"
|
|
||||||
: selectedTheme;
|
|
||||||
|
|
||||||
document.documentElement.setAttribute("data-bs-theme", effectiveTheme);
|
const { setMode } = useColorScheme();
|
||||||
};
|
|
||||||
|
|
||||||
applyTheme(theme);
|
|
||||||
|
|
||||||
// Save theme preference
|
|
||||||
localStorage.setItem("theme", theme);
|
|
||||||
}, [theme]);
|
|
||||||
|
|
||||||
// Shell type management
|
|
||||||
useEffect(() => {
|
|
||||||
localStorage.setItem("jmespath-shell-type", shellType);
|
|
||||||
}, [shellType]);
|
|
||||||
|
|
||||||
// Get headers for API requests
|
|
||||||
const getApiHeaders = () => {
|
|
||||||
return {
|
|
||||||
Accept: "application/json",
|
|
||||||
"X-API-Key": apiKey,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Load sample data from API on startup and setup periodic state checking
|
// Load sample data from API on startup and setup periodic state checking
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
// Sync initial theme from localStorage with MUI color scheme
|
||||||
|
const initialMode = theme === 'auto' ? 'system' : theme;
|
||||||
|
setMode(initialMode);
|
||||||
|
|
||||||
loadSampleData();
|
loadSampleData();
|
||||||
|
|
||||||
// Check for state changes every 5 seconds
|
// Check for state changes every 5 seconds
|
||||||
@@ -165,48 +148,73 @@ function App() {
|
|||||||
|
|
||||||
const handleThemeChange = (newTheme) => {
|
const handleThemeChange = (newTheme) => {
|
||||||
setTheme(newTheme);
|
setTheme(newTheme);
|
||||||
|
const muiMode = newTheme === "auto" ? "system" : newTheme;
|
||||||
|
setMode(muiMode);
|
||||||
|
localStorage.setItem("theme", newTheme);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePageChange = (newPage) => {
|
const handlePageChange = (newPage) => {
|
||||||
setCurrentPage(newPage);
|
setCurrentPage(newPage);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleShellTypeChange = (newShellType) => {
|
||||||
|
setShellType(newShellType);
|
||||||
|
localStorage.setItem("jmespath-shell-type", newShellType);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="vh-100 d-flex flex-column">
|
<>
|
||||||
<Header
|
<CssBaseline />
|
||||||
theme={theme}
|
<Box
|
||||||
onThemeChange={handleThemeChange}
|
sx={{
|
||||||
currentPage={currentPage}
|
display: "flex",
|
||||||
onPageChange={handlePageChange}
|
flexDirection: "column",
|
||||||
/>
|
height: "100vh",
|
||||||
|
overflow: "hidden",
|
||||||
{/* Main Content Section - flex-grow to fill space */}
|
bgcolor: "background.default",
|
||||||
<div
|
}}
|
||||||
className="container-fluid flex-grow-1 d-flex flex-column"
|
|
||||||
style={{ minHeight: 0 }}
|
|
||||||
>
|
>
|
||||||
{currentPage === "main" ? (
|
<Header
|
||||||
<MainPage
|
theme={theme}
|
||||||
apiKey={apiKey}
|
onThemeChange={handleThemeChange}
|
||||||
showReloadButton={showReloadButton}
|
currentPage={currentPage}
|
||||||
onReloadSampleData={loadSampleData}
|
onPageChange={handlePageChange}
|
||||||
jmespathExpression={jmespathExpression}
|
/>
|
||||||
setJmespathExpression={setJmespathExpression}
|
|
||||||
jsonData={jsonData}
|
|
||||||
setJsonData={setJsonData}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<ApiKeyPage
|
|
||||||
apiKey={apiKey}
|
|
||||||
onRegenerateApiKey={regenerateApiKey}
|
|
||||||
shellType={shellType}
|
|
||||||
onShellTypeChange={setShellType}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Footer />
|
{/* Main Content Section - flex-grow to fill space */}
|
||||||
</div>
|
<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>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ describe('App Component', () => {
|
|||||||
describe('Basic Rendering', () => {
|
describe('Basic Rendering', () => {
|
||||||
test('renders JMESPath Testing Tool title', () => {
|
test('renders JMESPath Testing Tool title', () => {
|
||||||
render(<App />);
|
render(<App />);
|
||||||
const titleElement = screen.getByRole('heading', { name: /JMESPath Testing Tool/i });
|
const titleElement = screen.getByText(/JMESPath Playground/i);
|
||||||
expect(titleElement).toBeInTheDocument();
|
expect(titleElement).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -90,11 +90,11 @@ describe('App Component', () => {
|
|||||||
|
|
||||||
test('renders all toolbar buttons', () => {
|
test('renders all toolbar buttons', () => {
|
||||||
render(<App />);
|
render(<App />);
|
||||||
expect(screen.getByTitle('Load JSON object from file')).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: /Load from Disk/i })).toBeInTheDocument();
|
||||||
expect(screen.getByTitle('Load JSON Lines log file')).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: /Load Logs/i })).toBeInTheDocument();
|
||||||
expect(screen.getByTitle('Load sample data')).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: /Load Sample/i })).toBeInTheDocument();
|
||||||
expect(screen.getByTitle('Format JSON')).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: /Format/i })).toBeInTheDocument();
|
||||||
expect(screen.getByTitle('Clear all inputs')).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);
|
const resultArea = screen.getByPlaceholderText(/Results will appear here/i);
|
||||||
|
|
||||||
// Clear all inputs first to start fresh
|
// 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);
|
await user.click(clearButton);
|
||||||
|
|
||||||
// Set JSON data directly after clearing
|
// Set JSON data directly after clearing
|
||||||
@@ -153,7 +153,7 @@ describe('App Component', () => {
|
|||||||
const jsonInput = screen.getByPlaceholderText(/Enter JSON data here/i);
|
const jsonInput = screen.getByPlaceholderText(/Enter JSON data here/i);
|
||||||
|
|
||||||
// Clear all inputs first
|
// 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);
|
await user.click(clearButton);
|
||||||
|
|
||||||
// Set invalid JSON directly
|
// Set invalid JSON directly
|
||||||
@@ -187,31 +187,55 @@ describe('App Component', () => {
|
|||||||
test('renders theme switcher buttons', () => {
|
test('renders theme switcher buttons', () => {
|
||||||
render(<App />);
|
render(<App />);
|
||||||
|
|
||||||
expect(screen.getByTitle('Auto (follow system)')).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: /Auto/i })).toBeInTheDocument();
|
||||||
expect(screen.getByTitle('Light theme')).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: /Light/i })).toBeInTheDocument();
|
||||||
expect(screen.getByTitle('Dark theme')).toBeInTheDocument();
|
expect(screen.getByRole('button', { name: /Dark/i })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('switches to light theme when clicked', async () => {
|
test('switches to light theme when clicked', async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
render(<App />);
|
render(<App />);
|
||||||
|
|
||||||
const lightButton = screen.getByTitle('Light theme');
|
const lightButton = screen.getByRole('button', { name: /Light/i });
|
||||||
await user.click(lightButton);
|
await user.click(lightButton);
|
||||||
|
|
||||||
// Check if button becomes active
|
// Check if button becomes active
|
||||||
expect(lightButton).toHaveClass('btn-primary');
|
expect(lightButton).toHaveClass('Mui-selected');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('switches to dark theme when clicked', async () => {
|
test('switches to dark theme when clicked', async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
render(<App />);
|
render(<App />);
|
||||||
|
|
||||||
const darkButton = screen.getByTitle('Dark theme');
|
const darkButton = screen.getByRole('button', { name: /Dark/i });
|
||||||
await user.click(darkButton);
|
await user.click(darkButton);
|
||||||
|
|
||||||
// Check if button becomes active
|
// 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 jmespathInput = screen.getByPlaceholderText(/Enter JMESPath expression/i);
|
||||||
const jsonInput = screen.getByPlaceholderText(/Enter JSON data here/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
|
// Add some content
|
||||||
await user.type(jmespathInput, 'test.expression');
|
await user.type(jmespathInput, 'test.expression');
|
||||||
@@ -241,7 +265,7 @@ describe('App Component', () => {
|
|||||||
render(<App />);
|
render(<App />);
|
||||||
|
|
||||||
const jsonInput = screen.getByPlaceholderText(/Enter JSON data here/i);
|
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
|
// Add minified JSON directly
|
||||||
fireEvent.change(jsonInput, { target: { value: '{"name":"Alice","age":30,"skills":["React","Node"]}' } });
|
fireEvent.change(jsonInput, { target: { value: '{"name":"Alice","age":30,"skills":["React","Node"]}' } });
|
||||||
@@ -260,7 +284,7 @@ describe('App Component', () => {
|
|||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
render(<App />);
|
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 jsonInput = screen.getByPlaceholderText(/Enter JSON data here/i);
|
||||||
const jmespathInput = screen.getByPlaceholderText(/Enter JMESPath expression/i);
|
const jmespathInput = screen.getByPlaceholderText(/Enter JMESPath expression/i);
|
||||||
|
|
||||||
@@ -331,7 +355,7 @@ describe('App Component', () => {
|
|||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
render(<App />);
|
render(<App />);
|
||||||
|
|
||||||
const loadObjectButton = screen.getByTitle('Load JSON object from file');
|
const loadObjectButton = screen.getByRole('button', { name: "Load from Disk" });
|
||||||
|
|
||||||
// Create a mock file
|
// Create a mock file
|
||||||
const file = new File(['{"test": "file data"}'], 'test.json', {
|
const file = new File(['{"test": "file data"}'], 'test.json', {
|
||||||
|
|||||||
@@ -1,4 +1,23 @@
|
|||||||
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 CodeBlock({ code }) {
|
function CodeBlock({ code }) {
|
||||||
const [copySuccess, setCopySuccess] = useState(false);
|
const [copySuccess, setCopySuccess] = useState(false);
|
||||||
@@ -9,28 +28,56 @@ function CodeBlock({ code }) {
|
|||||||
setCopySuccess(true);
|
setCopySuccess(true);
|
||||||
setTimeout(() => setCopySuccess(false), 2000);
|
setTimeout(() => setCopySuccess(false), 2000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to copy to clipboard:', err);
|
console.error("Failed to copy to clipboard:", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="position-relative">
|
<Box sx={{ position: "relative", my: 2 }}>
|
||||||
<pre className="bg-light p-3 pe-5 rounded border shadow-sm">
|
<Paper
|
||||||
<code className="d-block" style={{ whiteSpace: 'pre-wrap' }}>{code}</code>
|
variant="outlined"
|
||||||
</pre>
|
sx={{
|
||||||
<button
|
p: 2,
|
||||||
className={`btn btn-sm ${copySuccess ? 'btn-success' : 'btn-outline-secondary'} position-absolute top-0 end-0 m-2`}
|
pr: 6,
|
||||||
onClick={handleCopy}
|
bgcolor: "action.hover",
|
||||||
title="Copy code to clipboard"
|
fontFamily: "'Noto Sans Mono', monospace",
|
||||||
style={{ opacity: 0.8 }}
|
fontSize: "0.85rem",
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
|
wordBreak: "break-all",
|
||||||
|
position: "relative",
|
||||||
|
borderColor: "divider",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{copySuccess ? '✓' : '📋'}
|
<code>{code}</code>
|
||||||
</button>
|
<Tooltip title={copySuccess ? "Copied!" : "Copy code"}>
|
||||||
</div>
|
<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 }) {
|
function ApiKeyPage({
|
||||||
|
apiKey,
|
||||||
|
onRegenerateApiKey,
|
||||||
|
shellType,
|
||||||
|
onShellTypeChange,
|
||||||
|
}) {
|
||||||
const [copySuccess, setCopySuccess] = useState(false);
|
const [copySuccess, setCopySuccess] = useState(false);
|
||||||
|
|
||||||
const handleCopyToClipboard = async () => {
|
const handleCopyToClipboard = async () => {
|
||||||
@@ -39,108 +86,120 @@ function ApiKeyPage({ apiKey, onRegenerateApiKey, shellType, onShellTypeChange }
|
|||||||
setCopySuccess(true);
|
setCopySuccess(true);
|
||||||
setTimeout(() => setCopySuccess(false), 2000);
|
setTimeout(() => setCopySuccess(false), 2000);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Failed to copy to clipboard:', err);
|
console.error("Failed to copy to clipboard:", err);
|
||||||
// Fallback for older browsers
|
|
||||||
const textArea = document.createElement('textarea');
|
|
||||||
textArea.value = apiKey;
|
|
||||||
document.body.appendChild(textArea);
|
|
||||||
textArea.select();
|
|
||||||
document.execCommand('copy');
|
|
||||||
document.body.removeChild(textArea);
|
|
||||||
setCopySuccess(true);
|
|
||||||
setTimeout(() => setCopySuccess(false), 2000);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="row justify-content-center">
|
<Box sx={{ flexGrow: 1, py: 4, px: 2 }}>
|
||||||
<div className="col-md-8">
|
<Grid container justifyContent="center">
|
||||||
<div className="card">
|
<Grid size={{ xs: 12, md: 10, lg: 8 }}>
|
||||||
<div className="card-header">
|
<Paper elevation={1} sx={{ p: { xs: 3, md: 5 }, bgcolor: "background.paper", border: 1, borderColor: "divider" }}>
|
||||||
<h5 className="mb-0">🔐 API Key Management</h5>
|
<Typography variant="h5" gutterBottom sx={{ mb: 4, fontWeight: 700, display: "flex", alignItems: "center", gap: 1.5, color: "text.primary" }}>
|
||||||
</div>
|
<KeyIcon color="primary" /> API Key Management
|
||||||
<div className="card-body">
|
</Typography>
|
||||||
<div className="mb-4">
|
|
||||||
<label className="form-label fw-bold">Your API Key:</label>
|
<Box sx={{ mb: 6 }}>
|
||||||
<div className="input-group">
|
<Typography variant="subtitle2" gutterBottom color="text.secondary">
|
||||||
<input
|
YOUR API KEY
|
||||||
type="text"
|
</Typography>
|
||||||
className="form-control font-monospace"
|
<Box sx={{ display: "flex", gap: 1.5, alignItems: "center" }}>
|
||||||
value={apiKey}
|
<TextField
|
||||||
readOnly
|
fullWidth
|
||||||
/>
|
value={apiKey}
|
||||||
<button
|
slotProps={{
|
||||||
className={`btn ${copySuccess ? 'btn-success' : 'btn-outline-secondary'}`}
|
input: {
|
||||||
onClick={handleCopyToClipboard}
|
readOnly: true,
|
||||||
title="Copy API key to clipboard"
|
style: { fontFamily: "'Noto Sans Mono', monospace", fontSize: "0.9rem" },
|
||||||
>
|
},
|
||||||
{copySuccess ? '✓ Copied!' : '📋 Copy'}
|
}}
|
||||||
</button>
|
variant="outlined"
|
||||||
<button
|
sx={{ "& .MuiOutlinedInput-root": { bgcolor: "background.paper" } }}
|
||||||
className="btn btn-outline-primary"
|
/>
|
||||||
onClick={onRegenerateApiKey}
|
<Tooltip title="Copy API Key">
|
||||||
title="Generate new API key"
|
<IconButton
|
||||||
>
|
onClick={handleCopyToClipboard}
|
||||||
🔄 Regenerate
|
color={copySuccess ? "success" : "primary"}
|
||||||
</button>
|
size="medium"
|
||||||
</div>
|
sx={{ border: 1, borderColor: "divider" }}
|
||||||
<div className="form-text">
|
>
|
||||||
This API key is used to encrypt and authenticate data uploads.
|
{copySuccess ? <CheckIcon /> : <ContentCopyIcon />}
|
||||||
</div>
|
</IconButton>
|
||||||
</div>
|
</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>
|
||||||
|
|
||||||
<div className="mb-4">
|
|
||||||
<div className="d-flex justify-content-between align-items-center mb-2">
|
|
||||||
<h6 className="mb-0">📡 Remote Data Upload API</h6>
|
|
||||||
<div className="btn-group btn-group-sm" role="group" aria-label="Shell type selector">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
className="btn-check"
|
|
||||||
name="shellType"
|
|
||||||
id="shellBash"
|
|
||||||
autoComplete="off"
|
|
||||||
checked={shellType === 'bash'}
|
|
||||||
onChange={() => onShellTypeChange('bash')}
|
|
||||||
/>
|
|
||||||
<label className="btn btn-outline-secondary" htmlFor="shellBash">UNIX (Bash)</label>
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
className="btn-check"
|
|
||||||
name="shellType"
|
|
||||||
id="shellPowerShell"
|
|
||||||
autoComplete="off"
|
|
||||||
checked={shellType === 'powershell'}
|
|
||||||
onChange={() => onShellTypeChange('powershell')}
|
|
||||||
/>
|
|
||||||
<label className="btn btn-outline-secondary" htmlFor="shellPowerShell">Windows (PowerShell)</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-muted">
|
|
||||||
External tools can upload sample data remotely using the REST API.
|
|
||||||
The API key is required for authentication. Define two
|
|
||||||
environment variables in your {shellType === 'bash' ? <code>.bashrc</code> : <code>PowerShell profile</code>}.
|
|
||||||
</p>
|
|
||||||
<CodeBlock
|
<CodeBlock
|
||||||
code={shellType === 'bash'
|
code={
|
||||||
? `export JMESPATH_PLAYGROUND_API_URL=${window.location.origin}\nexport JMESPATH_PLAYGROUND_API_KEY=${apiKey}`
|
shellType === "bash"
|
||||||
: `$env:JMESPATH_PLAYGROUND_API_URL = "${window.location.origin}"\n$env:JMESPATH_PLAYGROUND_API_KEY = "${apiKey}"`}
|
? `export JMESPATH_PLAYGROUND_API_URL="${window.location.origin}"\nexport JMESPATH_PLAYGROUND_API_KEY="${apiKey}"`
|
||||||
|
: `$env:JMESPATH_PLAYGROUND_API_URL = "${window.location.origin}"\n$env:JMESPATH_PLAYGROUND_API_KEY = "${apiKey}"`
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<p className="text-muted">Then, use the following {shellType === 'bash' ? <code>curl</code> : <code>PowerShell</code>} command to upload your data:</p>
|
|
||||||
<CodeBlock
|
<CodeBlock
|
||||||
code={shellType === 'bash'
|
code={
|
||||||
? `curl -s -X POST \\\n -H "Content-Type: application/json" \\\n -H "Accept: application/json" \\\n -H "X-API-Key: $JMESPATH_PLAYGROUND_API_KEY" \\\n --data @__JSON_FILE_NAME__ \\\n "$JMESPATH_PLAYGROUND_API_URL/api/v1/upload"`
|
shellType === "bash"
|
||||||
: `Invoke-RestMethod -Uri "$env:JMESPATH_PLAYGROUND_API_URL/api/v1/upload" \\\n -Method Post \\\n -ContentType "application/json" \\\n -Headers @{\n "X-API-Key" = $env:JMESPATH_PLAYGROUND_API_KEY\n "Accept" = "application/json"\n } \\\n -InFile __JSON_FILE_NAME__`}
|
? `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" } ] }'`
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
<div className="form-text">
|
</Box>
|
||||||
Replace <code>{"__JSON_FILE_NAME__"}</code> with the path to your
|
</Paper>
|
||||||
JSON file containing the sample data. {shellType === 'bash' && <span>or use <code>-</code> to read from standard input.</span>}
|
</Grid>
|
||||||
</div>
|
</Grid>
|
||||||
</div>
|
</Box>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ApiKeyPage;
|
export default ApiKeyPage;
|
||||||
|
|||||||
@@ -1,27 +1,57 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import { VERSION } from '../version';
|
import { Box, Typography, Container, Link, Grid } from "@mui/material";
|
||||||
|
import { VERSION } from "../version";
|
||||||
|
|
||||||
function Footer() {
|
function Footer() {
|
||||||
return (
|
return (
|
||||||
<footer className="bg-light border-top mt-2 py-2 flex-shrink-0">
|
<Box
|
||||||
<div className="container">
|
component="footer"
|
||||||
<div className="row">
|
sx={{
|
||||||
<div className="col-md-6">
|
py: 1,
|
||||||
<p className="mb-0 text-muted small">
|
borderTop: 1,
|
||||||
<strong>JMESPath Testing Tool</strong> {VERSION === 'unknown' ? VERSION : `v${VERSION}`} - Created for testing and validating JMESPath expressions
|
borderColor: "divider",
|
||||||
</p>
|
bgcolor: "background.paper",
|
||||||
</div>
|
flexShrink: 0,
|
||||||
<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> |
|
<Container maxWidth="xl">
|
||||||
<a href="https://jmespath.org/" target="_blank" rel="noopener noreferrer" className="text-decoration-none ms-2">
|
<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
|
Learn JMESPath
|
||||||
</a>
|
</Link>
|
||||||
</p>
|
</Typography>
|
||||||
</div>
|
</Grid>
|
||||||
</div>
|
</Grid>
|
||||||
</div>
|
</Container>
|
||||||
</footer>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,72 +1,89 @@
|
|||||||
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 }) {
|
function Header({ theme, onThemeChange, currentPage, onPageChange }) {
|
||||||
return (
|
return (
|
||||||
<div className="header-section">
|
<AppBar position="static" color="default" elevation={1} sx={{ borderBottom: 1, borderColor: "divider" }}>
|
||||||
<div className="container-fluid px-4">
|
<Container maxWidth="xl">
|
||||||
<div className="row align-items-center">
|
<Toolbar disableGutters sx={{ display: "flex", justifyContent: "space-between", height: 64 }}>
|
||||||
<div className="col-12 text-center position-relative">
|
{/* Brand/Title */}
|
||||||
<h2 className="mb-1">JMESPath Testing Tool</h2>
|
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||||
{/* Right side controls - better positioning */}
|
<Typography
|
||||||
<div className="position-absolute top-50 end-0 translate-middle-y d-flex align-items-center gap-2 me-4">
|
variant="h5"
|
||||||
{/* API Key Management Button - more prominent */}
|
noWrap
|
||||||
<button
|
component="div"
|
||||||
type="button"
|
sx={{
|
||||||
className={`btn btn-sm ${
|
fontWeight: 700,
|
||||||
currentPage === 'apikey'
|
color: "primary.main",
|
||||||
? 'btn-warning fw-bold text-dark'
|
letterSpacing: ".05rem",
|
||||||
: 'btn-outline-warning'
|
}}
|
||||||
}`}
|
>
|
||||||
onClick={() => onPageChange(currentPage === 'main' ? 'apikey' : 'main')}
|
JMESPath Playground
|
||||||
title="API Key Management"
|
</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")}
|
||||||
>
|
>
|
||||||
🔐 API Keys
|
{currentPage === "main" ? "API Keys" : "Home"}
|
||||||
</button>
|
</Button>
|
||||||
{/* Theme switcher with theme-aware classes */}
|
</Tooltip>
|
||||||
<div className="btn-group btn-group-sm" role="group" aria-label="Theme switcher">
|
|
||||||
<button
|
<Divider orientation="vertical" flexItem sx={{ my: 2, mx: 1 }} />
|
||||||
type="button"
|
|
||||||
className={`btn ${
|
{/* Theme switcher */}
|
||||||
theme === 'auto'
|
<ToggleButtonGroup
|
||||||
? 'btn-light active'
|
value={theme}
|
||||||
: 'btn-outline-light'
|
exclusive
|
||||||
}`}
|
onChange={(e, nextTheme) => nextTheme && onThemeChange(nextTheme)}
|
||||||
onClick={() => onThemeChange('auto')}
|
aria-label="theme switcher"
|
||||||
title="Auto (follow system)"
|
size="small"
|
||||||
>
|
>
|
||||||
🌓 Auto
|
<Tooltip title="Follow system theme">
|
||||||
</button>
|
<ToggleButton value="auto" aria-label="Auto">
|
||||||
<button
|
<BrightnessAutoIcon sx={{ fontSize: "1.2rem" }} />
|
||||||
type="button"
|
</ToggleButton>
|
||||||
className={`btn ${
|
</Tooltip>
|
||||||
theme === 'light'
|
<Tooltip title="Light mode">
|
||||||
? 'btn-light active'
|
<ToggleButton value="light" aria-label="Light">
|
||||||
: 'btn-outline-light'
|
<LightModeIcon sx={{ fontSize: "1.2rem" }} />
|
||||||
}`}
|
</ToggleButton>
|
||||||
onClick={() => onThemeChange('light')}
|
</Tooltip>
|
||||||
title="Light theme"
|
<Tooltip title="Dark mode">
|
||||||
>
|
<ToggleButton value="dark" aria-label="Dark">
|
||||||
☀️ Light
|
<DarkModeIcon sx={{ fontSize: "1.2rem" }} />
|
||||||
</button>
|
</ToggleButton>
|
||||||
<button
|
</Tooltip>
|
||||||
type="button"
|
</ToggleButtonGroup>
|
||||||
className={`btn ${
|
</Box>
|
||||||
theme === 'dark'
|
</Toolbar>
|
||||||
? 'btn-light active'
|
</Container>
|
||||||
: 'btn-outline-light'
|
</AppBar>
|
||||||
}`}
|
|
||||||
onClick={() => onThemeChange('dark')}
|
|
||||||
title="Dark theme"
|
|
||||||
>
|
|
||||||
🌙 Dark
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Header;
|
export default Header;
|
||||||
|
|||||||
@@ -1,4 +1,31 @@
|
|||||||
import React, { useState, useEffect } from "react";
|
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";
|
import jmespath from "jmespath";
|
||||||
|
|
||||||
function MainPage({
|
function MainPage({
|
||||||
@@ -160,173 +187,348 @@ function MainPage({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Box
|
||||||
{/* Description paragraph */}
|
sx={{
|
||||||
<div className="row mb-2">
|
flexGrow: 1,
|
||||||
<div className="col-12">
|
pt: 1,
|
||||||
<p className="text-muted text-center mb-2 small">
|
pb: 3,
|
||||||
Validate and test JMESPath expressions against JSON data in
|
px: { xs: 2, md: 4 },
|
||||||
real-time. Enter your JMESPath query and JSON data below to see the
|
display: "flex",
|
||||||
results instantly.
|
flexDirection: "column",
|
||||||
</p>
|
minHeight: 0,
|
||||||
</div>
|
overflow: "hidden",
|
||||||
</div>
|
}}
|
||||||
|
>
|
||||||
|
<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 */}
|
<Paper
|
||||||
<div className="row mb-2">
|
sx={{
|
||||||
<div className="col-12">
|
mb: 1,
|
||||||
<div className="card">
|
flexShrink: 0,
|
||||||
<div className="card-header py-2">
|
bgcolor: "background.paper",
|
||||||
<h6 className="mb-0">
|
border: 1,
|
||||||
<i className="bi bi-search me-2"></i>
|
borderColor: "divider",
|
||||||
JMESPath Expression
|
overflow: "hidden",
|
||||||
</h6>
|
mb: 2
|
||||||
</div>
|
}}
|
||||||
<div className="card-body">
|
>
|
||||||
<input
|
<Box
|
||||||
type="text"
|
sx={{
|
||||||
className={`form-control jmespath-input ${error ? "error" : "success"}`}
|
px: 2,
|
||||||
value={jmespathExpression}
|
py: 1,
|
||||||
onChange={handleJmespathChange}
|
bgcolor: "action.hover",
|
||||||
placeholder="Enter JMESPath expression (e.g., people[*].name)"
|
borderBottom: 1,
|
||||||
/>
|
borderColor: "divider",
|
||||||
<div
|
display: "flex",
|
||||||
className={`alert mt-2 mb-0 d-flex justify-content-between align-items-center ${error ? "alert-danger" : "alert-success"}`}
|
alignItems: "center",
|
||||||
>
|
justifyContent: "space-between",
|
||||||
<small className="mb-0">
|
}}
|
||||||
{error || "Expression is correct"}
|
>
|
||||||
</small>
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||||
{showReloadButton && (
|
<SearchIcon sx={{ fontSize: 20 }} color="primary" />
|
||||||
<button
|
<Typography variant="subtitle2" color="text.primary">
|
||||||
className="btn btn-light btn-sm ms-2 border"
|
JMESPath Expression
|
||||||
onClick={() => {
|
</Typography>
|
||||||
onReloadSampleData();
|
</Box>
|
||||||
}}
|
</Box>
|
||||||
title="New sample data is available"
|
<Box sx={{ p: 1.5, mt: 0.5 }}>
|
||||||
>
|
<TextField
|
||||||
<i className="bi bi-arrow-clockwise me-1"></i>
|
fullWidth
|
||||||
Reload Sample Data
|
size="small"
|
||||||
</button>
|
placeholder="Enter JMESPath expression (e.g., people[*].name)"
|
||||||
)}
|
value={jmespathExpression}
|
||||||
</div>
|
onChange={handleJmespathChange}
|
||||||
</div>
|
error={!!error}
|
||||||
</div>
|
helperText={error || " "}
|
||||||
</div>
|
sx={{
|
||||||
</div>
|
"& .MuiInputBase-root": {
|
||||||
|
fontFamily: "'Noto Sans Mono', monospace",
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
},
|
||||||
|
"& .MuiFormHelperText-root": {
|
||||||
|
mt: 0.75,
|
||||||
|
mb: -0.5,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
{/* Lower Middle Section: Input and Output Areas */}
|
<Grid container spacing={3} sx={{ flex: "1 1 0", minHeight: 0, height: 0 }}>
|
||||||
<div className="row flex-grow-1" style={{ minHeight: 0 }}>
|
<Grid size={{ xs: 12, md: 6 }} sx={{ display: "flex", flexDirection: "column", minHeight: 0 }}>
|
||||||
{/* Left Panel: JSON Data Input */}
|
<Paper
|
||||||
<div className="col-md-6">
|
sx={{
|
||||||
<div className="card h-100 d-flex flex-column">
|
flexGrow: 1,
|
||||||
<div className="card-header d-flex justify-content-between align-items-center py-2">
|
display: "flex",
|
||||||
<h6 className="mb-0">
|
flexDirection: "column",
|
||||||
<i className="bi bi-file-earmark-code me-2"></i>
|
overflow: "hidden",
|
||||||
JSON Data
|
bgcolor: "background.paper",
|
||||||
</h6>
|
border: 1,
|
||||||
<div>
|
borderColor: "divider",
|
||||||
<button
|
minHeight: 0,
|
||||||
className="btn btn-outline-success btn-sm me-2"
|
}}
|
||||||
onClick={loadFromDisk}
|
>
|
||||||
title="Load JSON object from file"
|
<Box
|
||||||
>
|
sx={{
|
||||||
📄 Load an Object
|
px: 2,
|
||||||
</button>
|
py: 1,
|
||||||
<button
|
bgcolor: "action.hover",
|
||||||
className="btn btn-outline-info btn-sm me-2"
|
borderBottom: 1,
|
||||||
onClick={loadLogFile}
|
borderColor: "divider",
|
||||||
title="Load JSON Lines log file"
|
display: "flex",
|
||||||
>
|
justifyContent: "space-between",
|
||||||
📋 Load a Log File
|
alignItems: "center",
|
||||||
</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"
|
|
||||||
>
|
|
||||||
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 flex-grow-1 d-flex flex-column"
|
|
||||||
style={{ minHeight: 0 }}
|
|
||||||
>
|
>
|
||||||
<textarea
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||||
className={`form-control json-input flex-grow-1 ${jsonError ? "error" : "success"}`}
|
<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}
|
value={jsonData}
|
||||||
onChange={handleJsonChange}
|
onChange={handleJsonChange}
|
||||||
placeholder="Enter JSON data here..."
|
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 && (
|
{jsonError && (
|
||||||
<div className="alert alert-danger mt-2 mb-0">
|
<Alert severity="error" sx={{ mt: 1, flexShrink: 0 }} variant="filled">
|
||||||
<small>{jsonError}</small>
|
{jsonError}
|
||||||
</div>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Box>
|
||||||
</div>
|
</Paper>
|
||||||
</div>
|
</Grid>
|
||||||
|
|
||||||
{/* Right Panel: Results */}
|
<Grid size={{ xs: 12, md: 6 }} sx={{ display: "flex", flexDirection: "column", minHeight: 0 }}>
|
||||||
<div className="col-md-6">
|
<Paper
|
||||||
<div className="card h-100 d-flex flex-column">
|
sx={{
|
||||||
<div className="card-header py-2 d-flex justify-content-between align-items-center">
|
flexGrow: 1,
|
||||||
<h6 className="mb-0">
|
display: "flex",
|
||||||
<i className="bi bi-output me-2"></i>
|
flexDirection: "column",
|
||||||
Results
|
overflow: "hidden",
|
||||||
</h6>
|
bgcolor: "background.paper",
|
||||||
<div>
|
border: 1,
|
||||||
<button
|
borderColor: "divider",
|
||||||
className={`btn btn-sm me-2 ${copySuccess ? "btn-success" : "btn-outline-secondary"}`}
|
minHeight: 0,
|
||||||
onClick={copyToClipboard}
|
}}
|
||||||
disabled={!result || result === "null"}
|
>
|
||||||
title="Copy result to clipboard"
|
<Box
|
||||||
>
|
sx={{
|
||||||
<i className={`bi ${copySuccess ? "bi-check-lg" : "bi-clipboard"} me-1`}></i>
|
px: 2,
|
||||||
{copySuccess ? "Copied!" : "Copy"}
|
py: 1,
|
||||||
</button>
|
bgcolor: "action.hover",
|
||||||
<button
|
borderBottom: 1,
|
||||||
className="btn btn-outline-secondary btn-sm"
|
borderColor: "divider",
|
||||||
onClick={downloadResult}
|
display: "flex",
|
||||||
disabled={!result || result === "null"}
|
justifyContent: "space-between",
|
||||||
title="Download result as JSON file"
|
alignItems: "center",
|
||||||
>
|
}}
|
||||||
<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
|
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||||
className="form-control result-output flex-grow-1"
|
<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}
|
value={result}
|
||||||
readOnly
|
variant="standard"
|
||||||
placeholder="Results will appear here..."
|
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>
|
</Box>
|
||||||
</div>
|
</Paper>
|
||||||
</div>
|
</Grid>
|
||||||
</div>
|
</Grid>
|
||||||
</>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background-color: #f8f9fa;
|
background-color: #f8f9fa;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from "react-dom/client";
|
||||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
import { ThemeProvider } from "@mui/material";
|
||||||
import './index.css';
|
import theme from "./theme";
|
||||||
import App from './App';
|
import "./index.css";
|
||||||
|
import App from "./App";
|
||||||
|
|
||||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||||
root.render(
|
root.render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<ThemeProvider theme={theme} defaultMode="system">
|
||||||
|
<App />
|
||||||
|
</ThemeProvider>
|
||||||
</React.StrictMode>
|
</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;
|
||||||
Reference in New Issue
Block a user