534 lines
16 KiB
JavaScript
534 lines
16 KiB
JavaScript
import React, { useState, useEffect } from "react";
|
|
import {
|
|
Box,
|
|
Typography,
|
|
Paper,
|
|
TextField,
|
|
Button,
|
|
Tooltip,
|
|
IconButton,
|
|
Alert,
|
|
Stack,
|
|
Divider,
|
|
} from "@mui/material";
|
|
import {
|
|
Search as SearchIcon,
|
|
DataObject as DataObjectIcon,
|
|
Output as OutputIcon,
|
|
UploadFile as UploadFileIcon,
|
|
FileOpen as FileOpenIcon,
|
|
Restore as RestoreIcon,
|
|
FormatAlignLeft as FormatAlignLeftIcon,
|
|
Clear as ClearIcon,
|
|
ContentCopy as ContentCopyIcon,
|
|
Download as DownloadIcon,
|
|
Check as CheckIcon,
|
|
Refresh as RefreshIcon,
|
|
} from "@mui/icons-material";
|
|
import Grid from "@mui/material/Grid";
|
|
import jmespath from "jmespath";
|
|
|
|
function MainPage({
|
|
showReloadButton,
|
|
onReloadSampleData,
|
|
jmespathExpression,
|
|
setJmespathExpression,
|
|
jsonData,
|
|
setJsonData,
|
|
}) {
|
|
const [result, setResult] = useState("");
|
|
const [error, setError] = useState("");
|
|
const [jsonError, setJsonError] = useState("");
|
|
const [copySuccess, setCopySuccess] = useState(false);
|
|
|
|
const evaluateExpression = () => {
|
|
try {
|
|
// Clear previous errors
|
|
setError("");
|
|
setJsonError("");
|
|
|
|
// Validate and parse JSON
|
|
let parsedData;
|
|
try {
|
|
parsedData = JSON.parse(jsonData);
|
|
} catch (jsonErr) {
|
|
setJsonError(`Invalid JSON: ${jsonErr.message}`);
|
|
setResult("");
|
|
return;
|
|
}
|
|
|
|
// Evaluate JMESPath expression
|
|
const queryResult = jmespath.search(parsedData, jmespathExpression);
|
|
|
|
// Format the result
|
|
if (queryResult === null || queryResult === undefined) {
|
|
setResult("null");
|
|
} else {
|
|
setResult(JSON.stringify(queryResult, null, 2));
|
|
}
|
|
} catch (jmesErr) {
|
|
setError(`JMESPath Error: ${jmesErr.message}`);
|
|
setResult("");
|
|
}
|
|
};
|
|
|
|
// Auto-evaluate when inputs change
|
|
useEffect(() => {
|
|
if (jmespathExpression && jsonData) {
|
|
evaluateExpression();
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [jmespathExpression, jsonData]);
|
|
|
|
const handleJmespathChange = (e) => {
|
|
setJmespathExpression(e.target.value);
|
|
};
|
|
|
|
const handleJsonChange = (e) => {
|
|
setJsonData(e.target.value);
|
|
};
|
|
|
|
const formatJson = () => {
|
|
try {
|
|
const parsed = JSON.parse(jsonData);
|
|
setJsonData(JSON.stringify(parsed, null, 2));
|
|
} catch (err) {
|
|
// If JSON is invalid, don't format
|
|
}
|
|
};
|
|
|
|
const clearAll = () => {
|
|
setJmespathExpression("");
|
|
setJsonData("");
|
|
setResult("");
|
|
setError("");
|
|
setJsonError("");
|
|
};
|
|
|
|
const copyToClipboard = async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(result);
|
|
setCopySuccess(true);
|
|
setTimeout(() => setCopySuccess(false), 2000);
|
|
} catch (err) {
|
|
console.error("Failed to copy!", err);
|
|
}
|
|
};
|
|
|
|
const downloadResult = () => {
|
|
const blob = new Blob([result], { type: "application/json" });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = "result.json";
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
};
|
|
|
|
const loadSample = () => {
|
|
const sampleData = {
|
|
users: [
|
|
{ name: "Alice", age: 30, city: "New York" },
|
|
{ name: "Bob", age: 25, city: "San Francisco" },
|
|
{ name: "Charlie", age: 35, city: "Chicago" },
|
|
],
|
|
total: 3,
|
|
};
|
|
setJsonData(JSON.stringify(sampleData, null, 2));
|
|
setJmespathExpression("users[?age > `30`].name");
|
|
};
|
|
|
|
const loadFromDisk = () => {
|
|
const input = document.createElement("input");
|
|
input.type = "file";
|
|
input.accept = ".json";
|
|
input.onchange = (e) => {
|
|
const file = e.target.files[0];
|
|
if (file) {
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
try {
|
|
const content = e.target.result;
|
|
const parsed = JSON.parse(content);
|
|
setJsonData(JSON.stringify(parsed, null, 2));
|
|
} catch (error) {
|
|
alert("Invalid JSON file");
|
|
}
|
|
};
|
|
reader.readAsText(file);
|
|
}
|
|
};
|
|
input.click();
|
|
};
|
|
|
|
const loadLogFile = () => {
|
|
const input = document.createElement("input");
|
|
input.type = "file";
|
|
input.accept = ".log,.jsonl,.ndjson";
|
|
input.onchange = (e) => {
|
|
const file = e.target.files[0];
|
|
if (file) {
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
try {
|
|
const content = e.target.result;
|
|
const lines = content.trim().split("\n");
|
|
const logs = lines.map((line) => JSON.parse(line));
|
|
setJsonData(JSON.stringify(logs, null, 2));
|
|
setJmespathExpression("[*].message");
|
|
} catch (error) {
|
|
alert("Invalid JSON Lines file");
|
|
}
|
|
};
|
|
reader.readAsText(file);
|
|
}
|
|
};
|
|
input.click();
|
|
};
|
|
|
|
return (
|
|
<Box
|
|
sx={{
|
|
flexGrow: 1,
|
|
pt: 1,
|
|
pb: 3,
|
|
px: { xs: 2, md: 4 },
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
minHeight: 0,
|
|
overflow: "hidden",
|
|
}}
|
|
>
|
|
<Box sx={{ mb: 2, flexShrink: 0 }}>
|
|
<Typography
|
|
variant="body2"
|
|
color="text.secondary"
|
|
align="left"
|
|
>
|
|
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>
|
|
|
|
<Paper
|
|
sx={{
|
|
mb: 1,
|
|
flexShrink: 0,
|
|
bgcolor: "background.paper",
|
|
border: 1,
|
|
borderColor: "divider",
|
|
overflow: "hidden",
|
|
}}
|
|
>
|
|
<Box
|
|
sx={{
|
|
px: 2,
|
|
py: 1,
|
|
bgcolor: "action.hover",
|
|
borderBottom: 1,
|
|
borderColor: "divider",
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "space-between",
|
|
}}
|
|
>
|
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
|
<SearchIcon sx={{ fontSize: 20 }} color="primary" />
|
|
<Typography variant="subtitle2" color="text.primary">
|
|
JMESPath Expression
|
|
</Typography>
|
|
</Box>
|
|
</Box>
|
|
<Box sx={{ p: 1.5 }}>
|
|
<TextField
|
|
fullWidth
|
|
size="small"
|
|
placeholder="Enter JMESPath expression (e.g., people[*].name)"
|
|
value={jmespathExpression}
|
|
onChange={handleJmespathChange}
|
|
error={!!error}
|
|
helperText={error || " "}
|
|
sx={{
|
|
"& .MuiInputBase-root": {
|
|
fontFamily: "'Noto Sans Mono', monospace",
|
|
fontSize: "0.9rem",
|
|
},
|
|
"& .MuiFormHelperText-root": {
|
|
mt: 0.75,
|
|
mb: -0.5,
|
|
},
|
|
}}
|
|
/>
|
|
</Box>
|
|
</Paper>
|
|
|
|
<Grid container spacing={3} sx={{ flex: "1 1 0", minHeight: 0, height: 0 }}>
|
|
<Grid size={{ xs: 12, md: 6 }} sx={{ display: "flex", flexDirection: "column", minHeight: 0 }}>
|
|
<Paper
|
|
sx={{
|
|
flexGrow: 1,
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
overflow: "hidden",
|
|
bgcolor: "background.paper",
|
|
border: 1,
|
|
borderColor: "divider",
|
|
minHeight: 0,
|
|
}}
|
|
>
|
|
<Box
|
|
sx={{
|
|
px: 2,
|
|
py: 1,
|
|
bgcolor: "action.hover",
|
|
borderBottom: 1,
|
|
borderColor: "divider",
|
|
display: "flex",
|
|
justifyContent: "space-between",
|
|
alignItems: "center",
|
|
}}
|
|
>
|
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
|
<DataObjectIcon sx={{ fontSize: 20 }} color="primary" />
|
|
<Typography variant="subtitle2" color="text.primary">
|
|
JSON Input
|
|
</Typography>
|
|
{showReloadButton && (
|
|
<Button
|
|
variant="contained"
|
|
color="secondary"
|
|
onClick={onReloadSampleData}
|
|
startIcon={<RefreshIcon fontSize="inherit" />}
|
|
size="small"
|
|
sx={{
|
|
ml: 1,
|
|
px: 1,
|
|
py: 0.25,
|
|
fontSize: "0.65rem",
|
|
textTransform: "none",
|
|
whiteSpace: "nowrap",
|
|
minWidth: "auto",
|
|
}}
|
|
>
|
|
Reload data
|
|
</Button>
|
|
)}
|
|
</Box>
|
|
<Stack direction="row" spacing={1} alignItems="center">
|
|
<Tooltip title="Load from Disk">
|
|
<IconButton
|
|
size="small"
|
|
onClick={loadFromDisk}
|
|
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}
|
|
onChange={handleJsonChange}
|
|
placeholder="Enter JSON data here..."
|
|
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 && (
|
|
<Alert severity="error" sx={{ mt: 1, flexShrink: 0 }} variant="filled">
|
|
{jsonError}
|
|
</Alert>
|
|
)}
|
|
</Box>
|
|
</Paper>
|
|
</Grid>
|
|
|
|
<Grid size={{ xs: 12, md: 6 }} sx={{ display: "flex", flexDirection: "column", minHeight: 0 }}>
|
|
<Paper
|
|
sx={{
|
|
flexGrow: 1,
|
|
display: "flex",
|
|
flexDirection: "column",
|
|
overflow: "hidden",
|
|
bgcolor: "background.paper",
|
|
border: 1,
|
|
borderColor: "divider",
|
|
minHeight: 0,
|
|
}}
|
|
>
|
|
<Box
|
|
sx={{
|
|
px: 2,
|
|
py: 1,
|
|
bgcolor: "action.hover",
|
|
borderBottom: 1,
|
|
borderColor: "divider",
|
|
display: "flex",
|
|
justifyContent: "space-between",
|
|
alignItems: "center",
|
|
}}
|
|
>
|
|
<Box sx={{ display: "flex", alignItems: "center" }}>
|
|
<OutputIcon sx={{ mr: 1, fontSize: 20 }} color="primary" />
|
|
<Typography variant="subtitle2" color="text.primary">
|
|
Query Result
|
|
</Typography>
|
|
</Box>
|
|
<Stack direction="row" spacing={1}>
|
|
<Tooltip title="Copy to Clipboard">
|
|
<span>
|
|
<IconButton
|
|
size="small"
|
|
onClick={copyToClipboard}
|
|
disabled={!result || result === "null"}
|
|
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}
|
|
variant="standard"
|
|
placeholder="Results will appear here..."
|
|
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,
|
|
},
|
|
}}
|
|
/>
|
|
</Box>
|
|
</Paper>
|
|
</Grid>
|
|
</Grid>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
export default MainPage;
|