Converted to Material UI v7 - bare.
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user