Converted to Material UI v7 - bare.

This commit is contained in:
2026-01-31 11:52:15 +01:00
parent 57371feeb0
commit 3dd352df92
12 changed files with 1451 additions and 777 deletions

View File

@@ -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 }) {
const [copySuccess, setCopySuccess] = useState(false);
@@ -9,28 +28,57 @@ function CodeBlock({ code }) {
setCopySuccess(true);
setTimeout(() => setCopySuccess(false), 2000);
} catch (err) {
console.error('Failed to copy to clipboard:', err);
console.error("Failed to copy to clipboard:", err);
}
};
return (
<div className="position-relative">
<pre className="bg-light p-3 pe-5 rounded border shadow-sm">
<code className="d-block" style={{ whiteSpace: 'pre-wrap' }}>{code}</code>
</pre>
<button
className={`btn btn-sm ${copySuccess ? 'btn-success' : 'btn-outline-secondary'} position-absolute top-0 end-0 m-2`}
onClick={handleCopy}
title="Copy code to clipboard"
style={{ opacity: 0.8 }}
<Box sx={{ position: "relative", my: 2 }}>
<Paper
variant="outlined"
sx={{
p: 2,
pr: 6,
bgcolor: "action.hover",
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
fontSize: "0.85rem",
whiteSpace: "pre-wrap",
wordBreak: "break-all",
position: "relative",
borderRadius: 2,
borderColor: "divider",
}}
>
{copySuccess ? '✓' : '📋'}
</button>
</div>
<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 }) {
function ApiKeyPage({
apiKey,
onRegenerateApiKey,
shellType,
onShellTypeChange,
}) {
const [copySuccess, setCopySuccess] = useState(false);
const handleCopyToClipboard = async () => {
@@ -39,108 +87,118 @@ function ApiKeyPage({ apiKey, onRegenerateApiKey, shellType, onShellTypeChange }
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 component="main" sx={{ flexGrow: 1, py: 4, px: 2 }}>
<Grid container justifyContent="center">
<Grid size={{ xs: 12, md: 8, lg: 6 }}>
<Paper elevation={1} sx={{ p: { xs: 3, md: 5 }, borderRadius: 4, bgcolor: "background.paper", border: 1, borderColor: "divider" }}>
<Typography variant="h5" gutterBottom sx={{ mb: 4, fontWeight: 700, display: "flex", alignItems: "center", gap: 1.5, color: "text.primary" }}>
<KeyIcon color="primary" /> API Key Management
</Typography>
<Box sx={{ mb: 6 }}>
<Typography variant="subtitle2" gutterBottom color="text.secondary">
YOUR API KEY
</Typography>
<Box sx={{ display: "flex", gap: 1.5, alignItems: "center" }}>
<TextField
fullWidth
value={apiKey}
slotProps={{
input: {
readOnly: true,
style: { fontFamily: "'JetBrains Mono', 'Fira Code', monospace", fontSize: "0.9rem" },
},
}}
variant="outlined"
sx={{ "& .MuiOutlinedInput-root": { borderRadius: 4, 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"
>
<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
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}"`}
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}"`
}
/>
<p className="text-muted">Then, use the following {shellType === 'bash' ? <code>curl</code> : <code>PowerShell</code>} command to upload your data:</p>
<CodeBlock
code={shellType === 'bash'
? `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"`
: `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__`}
code={
shellType === "bash"
? `curl -X POST "$JMESPATH_PLAYGROUND_API_URL/api/v1/upload" \\
-H "Accept: application/json" \\
-H "x-api-key: $JMESPATH_PLAYGROUND_API_KEY" \\
-d '{ "users": [ { "id": 1, "name": "Remote User" } ] }'`
: `Invoke-RestMethod -Method Post -Uri "$env:JMESPATH_PLAYGROUND_API_URL/api/v1/upload" \`
-Headers @{ "Accept" = "application/json"; "x-api-key" = $env:JMESPATH_PLAYGROUND_API_KEY } \`
-Body '{ "users": [ { "id": 1, "name": "Remote User" } ] }'`
}
/>
<div className="form-text">
Replace <code>{"__JSON_FILE_NAME__"}</code> with the path to your
JSON file containing the sample data. {shellType === 'bash' && <span>or use <code>-</code> to read from standard input.</span>}
</div>
</div>
</div>
</div>
</div>
</div>
</Box>
</Paper>
</Grid>
</Grid>
</Box>
);
}
export default ApiKeyPage;
export default ApiKeyPage;

View File

@@ -1,27 +1,58 @@
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: 2,
mt: 2,
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>
);
}

View File

@@ -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 }) {
return (
<div className="header-section">
<div className="container-fluid px-4">
<div className="row align-items-center">
<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-50 end-0 translate-middle-y d-flex align-items-center gap-2 me-4">
{/* API Key Management Button - more prominent */}
<button
type="button"
className={`btn btn-sm ${
currentPage === 'apikey'
? 'btn-warning fw-bold text-dark'
: '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="h6"
noWrap
component="div"
sx={{
fontWeight: 700,
color: "primary.main",
letterSpacing: ".05rem",
}}
>
JMESPath Playground
</Typography>
</Box>
{/* Right side controls */}
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
{/* API Key Management Button */}
<Tooltip title={currentPage === "main" ? "API Key Management" : "Back to Testing"}>
<Button
variant={currentPage === "apikey" ? "contained" : "text"}
color={currentPage === "apikey" ? "primary" : "primary"}
size="medium"
startIcon={currentPage === "main" ? <KeyIcon /> : <HomeIcon />}
onClick={() => onPageChange(currentPage === "main" ? "apikey" : "main")}
>
🔐 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-light active'
: 'btn-outline-light'
}`}
onClick={() => onThemeChange('auto')}
title="Auto (follow system)"
>
🌓 Auto
</button>
<button
type="button"
className={`btn ${
theme === 'light'
? 'btn-light active'
: 'btn-outline-light'
}`}
onClick={() => onThemeChange('light')}
title="Light theme"
>
Light
</button>
<button
type="button"
className={`btn ${
theme === 'dark'
? 'btn-light active'
: 'btn-outline-light'
}`}
onClick={() => onThemeChange('dark')}
title="Dark theme"
>
🌙 Dark
</button>
</div>
</div>
</div>
</div>
</div>
</div>
{currentPage === "main" ? "API Keys" : "Home"}
</Button>
</Tooltip>
<Divider orientation="vertical" flexItem sx={{ my: 2, mx: 1 }} />
{/* Theme switcher */}
<ToggleButtonGroup
value={theme}
exclusive
onChange={(e, nextTheme) => nextTheme && onThemeChange(nextTheme)}
aria-label="theme switcher"
size="small"
>
<Tooltip title="Follow system theme">
<ToggleButton value="auto" aria-label="Auto">
<BrightnessAutoIcon sx={{ fontSize: "1.2rem" }} />
</ToggleButton>
</Tooltip>
<Tooltip title="Light mode">
<ToggleButton value="light" aria-label="Light">
<LightModeIcon sx={{ fontSize: "1.2rem" }} />
</ToggleButton>
</Tooltip>
<Tooltip title="Dark mode">
<ToggleButton value="dark" aria-label="Dark">
<DarkModeIcon sx={{ fontSize: "1.2rem" }} />
</ToggleButton>
</Tooltip>
</ToggleButtonGroup>
</Box>
</Toolbar>
</Container>
</AppBar>
);
}
export default Header;
export default Header;

View File

@@ -1,4 +1,31 @@
import React, { useState, useEffect } from "react";
import {
Box,
Typography,
Paper,
TextField,
Button,
Grid,
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 jmespath from "jmespath";
function MainPage({
@@ -160,173 +187,275 @@ 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 component="main" sx={{ flexGrow: 1, py: 3, px: { xs: 2, md: 4 } }}>
<Box sx={{ mb: 4, maxWidth: 800, mx: "auto" }}>
<Typography
variant="body1"
color="text.secondary"
align="center"
sx={{ mb: 3 }}
>
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>
JMESPath Expression
</h6>
</div>
<div className="card-body">
<input
type="text"
className={`form-control jmespath-input ${error ? "error" : "success"}`}
value={jmespathExpression}
onChange={handleJmespathChange}
placeholder="Enter JMESPath expression (e.g., people[*].name)"
/>
<div
className={`alert mt-2 mb-0 d-flex justify-content-between align-items-center ${error ? "alert-danger" : "alert-success"}`}
<Paper
sx={{
mb: 4,
p: 0,
borderRadius: 2,
overflow: "hidden",
bgcolor: "background.paper",
}}
>
<Box
sx={{
px: 3,
py: 1.5,
display: "flex",
alignItems: "center",
gap: 1.5,
bgcolor: "action.hover",
}}
>
<SearchIcon sx={{ fontSize: 24 }} color="primary" />
<Typography variant="subtitle1" fontWeight="600">
JMESPath Expression
</Typography>
</Box>
<Box sx={{ px: 3, pb: 2 }}>
<TextField
fullWidth
placeholder="Enter JMESPath expression (e.g., people[*].name)"
value={jmespathExpression}
onChange={handleJmespathChange}
error={!!error}
helperText={error || " "}
FormHelperTextProps={{
sx: { color: error ? "error.main" : "success.main", fontWeight: 500 }
}}
slotProps={{
input: {
style: { fontFamily: "'JetBrains Mono', 'Fira Code', monospace", fontSize: "1rem" },
},
}}
sx={{
"& .MuiOutlinedInput-root": {
borderRadius: 3,
bgcolor: "background.paper",
}
}}
/>
{showReloadButton && (
<Box sx={{ display: "flex", justifyContent: "center", mt: -1, mb: 2 }}>
<Button
variant="contained"
color="secondary"
onClick={onReloadSampleData}
startIcon={<RefreshIcon />}
size="small"
>
<small className="mb-0">
{error || "Expression is correct"}
</small>
{showReloadButton && (
<button
className="btn btn-light btn-sm ms-2 border"
onClick={() => {
onReloadSampleData();
}}
title="New sample data is available"
>
<i className="bi bi-arrow-clockwise me-1"></i>
Reload Sample Data
</button>
)}
</div>
</div>
</div>
</div>
</div>
New data available - Reload
</Button>
</Box>
)}
</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"
onClick={loadFromDisk}
title="Load JSON object from file"
>
📄 Load an Object
</button>
<button
className="btn btn-outline-info btn-sm me-2"
onClick={loadLogFile}
title="Load JSON Lines log file"
>
📋 Load a Log File
</button>
<button
className="btn btn-outline-primary btn-sm me-2"
onClick={loadSample}
title="Load sample data"
>
Load Sample
</button>
<button
className="btn btn-outline-secondary btn-sm me-2"
onClick={formatJson}
title="Format JSON"
>
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 }}
<Grid container spacing={3} sx={{ flexGrow: 1, minHeight: 0 }}>
<Grid size={{ xs: 12, md: 6 }} sx={{ display: "flex", flexDirection: "column" }}>
<Paper
sx={{
flexGrow: 1,
display: "flex",
flexDirection: "column",
borderRadius: 4,
overflow: "hidden",
bgcolor: "background.paper",
}}
>
<Box
sx={{
px: 2,
py: 2,
bgcolor: "action.hover",
borderBottom: 1,
borderColor: "divider",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
flexWrap: "wrap",
gap: 1,
}}
>
<textarea
className={`form-control json-input flex-grow-1 ${jsonError ? "error" : "success"}`}
<Box sx={{ display: "flex", alignItems: "center" }}>
<DataObjectIcon sx={{ mr: 1, fontSize: 20 }} color="primary" />
<Typography variant="subtitle2" color="text.primary">
JSON Input
</Typography>
</Box>
<Stack direction="row" spacing={1} flexWrap="wrap">
<Tooltip title="Load from Disk">
<span>
<IconButton size="medium" onClick={loadFromDisk} color="primary" aria-label="Load from Disk">
<FileOpenIcon fontSize="small" />
</IconButton>
</span>
</Tooltip>
<Tooltip title="Load Logs">
<span>
<IconButton size="medium" onClick={loadLogFile} color="primary" aria-label="Load Logs">
<UploadFileIcon fontSize="small" />
</IconButton>
</span>
</Tooltip>
<Tooltip title="Load Sample">
<span>
<IconButton size="medium" onClick={loadSample} color="primary" aria-label="Load Sample">
<RestoreIcon fontSize="small" />
</IconButton>
</span>
</Tooltip>
<Tooltip title="Format">
<span>
<IconButton size="medium" onClick={formatJson} color="primary" aria-label="Format">
<FormatAlignLeftIcon fontSize="small" />
</IconButton>
</span>
</Tooltip>
<Divider orientation="vertical" flexItem sx={{ mx: 0.5 }} />
<Tooltip title="Clear">
<span>
<IconButton size="medium" onClick={clearAll} color="secondary" aria-label="Clear all inputs">
<ClearIcon fontSize="small" />
</IconButton>
</span>
</Tooltip>
</Stack>
</Box>
<Box sx={{ p: 2, flexGrow: 1, display: "flex", flexDirection: "column" }}>
<TextField
multiline
fullWidth
value={jsonData}
onChange={handleJsonChange}
placeholder="Enter JSON data here..."
style={{ minHeight: 0, resize: "none" }}
error={!!jsonError}
variant="standard"
slotProps={{
input: {
disableUnderline: true,
style: {
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
height: "100%",
alignItems: "flex-start",
fontSize: "0.85rem",
lineHeight: 1.5,
},
},
}}
sx={{
flexGrow: 1,
"& .MuiInputBase-root": { height: "100%", overflow: "auto" },
}}
/>
{jsonError && (
<div className="alert alert-danger mt-2 mb-0">
<small>{jsonError}</small>
</div>
<Alert severity="error" sx={{ mt: 1, borderRadius: 2 }} 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"}`}
onClick={copyToClipboard}
disabled={!result || result === "null"}
title="Copy result to clipboard"
>
<i className={`bi ${copySuccess ? "bi-check-lg" : "bi-clipboard"} me-1`}></i>
{copySuccess ? "Copied!" : "Copy"}
</button>
<button
className="btn btn-outline-secondary btn-sm"
onClick={downloadResult}
disabled={!result || result === "null"}
title="Download result as JSON file"
>
<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 }}
<Grid size={{ xs: 12, md: 6 }} sx={{ display: "flex", flexDirection: "column" }}>
<Paper
sx={{
flexGrow: 1,
display: "flex",
flexDirection: "column",
borderRadius: 4,
overflow: "hidden",
bgcolor: "background.paper",
}}
>
<Box
sx={{
px: 2,
py: 2,
bgcolor: "action.hover",
borderBottom: 1,
borderColor: "divider",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<textarea
className="form-control result-output flex-grow-1"
<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="medium"
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="medium"
onClick={downloadResult}
disabled={!result || result === "null"}
color="primary"
>
<DownloadIcon fontSize="small" />
</IconButton>
</span>
</Tooltip>
</Stack>
</Box>
<Box sx={{ p: 2, flexGrow: 1, display: "flex", flexDirection: "column" }}>
<TextField
multiline
fullWidth
value={result}
readOnly
slotProps={{
input: {
readOnly: true,
disableUnderline: true,
style: {
fontFamily: "'JetBrains Mono', 'Fira Code', monospace",
height: "100%",
alignItems: "flex-start",
fontSize: "0.85rem",
lineHeight: 1.5,
},
},
}}
variant="standard"
placeholder="Results will appear here..."
style={{ minHeight: 0, resize: "none" }}
sx={{
flexGrow: 1,
"& .MuiInputBase-root": { height: "100%", overflow: "auto" },
}}
/>
</div>
</div>
</div>
</div>
</>
</Box>
</Paper>
</Grid>
</Grid>
</Box>
);
}