Compare commits
13 Commits
v1.3.1
...
2218581e78
| Author | SHA1 | Date | |
|---|---|---|---|
| 2218581e78 | |||
| c21c0f863e | |||
| bcc7983849 | |||
| fbb98b7f39 | |||
| d8bde75670 | |||
| 42e91f6ec1 | |||
| 44bb4b7458 | |||
| 794fd88e8d | |||
| 9f0d7ee70a | |||
| 4c964cdfeb | |||
| be6dc0de60 | |||
| dc9def4faf | |||
| 3dd352df92 |
115
.github/copilot-instructions.md
vendored
115
.github/copilot-instructions.md
vendored
@@ -4,106 +4,19 @@ applyTo: "**/*.md,**/.js"
|
|||||||
---
|
---
|
||||||
# AI Agent Instructions for JMESPath Testing Tool
|
# AI Agent Instructions for JMESPath Testing Tool
|
||||||
|
|
||||||
The tool in this repository is designed to help users validate and test JMESPath expressions against JSON data. It is a React-based web application that provides an interactive interface for entering JMESPath queries and viewing the results.
|
This repository contains a React-based web application that allows users to test JMESPath expressions against JSON data. The application includes both a frontend and a backend server.
|
||||||
|
|
||||||
The main application page is divided into three sections:
|
Coding Guidelines:
|
||||||
|
|
||||||
- Top section: Title and description of the tool.
|
1. Use React, Vite and JavaScript/TypeScript for development.
|
||||||
- Theme control buttons (auto/light/dark)
|
2. Check the current date to establish context for choosing versions and dependencies.
|
||||||
- Key-lock button that switches to the second application page.
|
3. Use Node.js 24 or higher LTS version.
|
||||||
- Middle section:
|
4. When asked, answer the question and provide explanations. Do not guess nor infer missing information. Report lack of information instead.
|
||||||
- The label "JMESPath Expression" with a right allinged row of action buttons:
|
5. When requested to make changes, do not modify unrelated parts of the code nor apply unapproved changes. Always present a change plan first, wait for approval, then implement the changes.
|
||||||
- Load an Object
|
6. Do not try to manage the files directly. Instead always use Git mv, rm, etc. commands to ensure proper tracking.
|
||||||
- Load a Log File
|
7. Do not run the development server(s) unless explicitly instructed to do so. Report the need to run the server for testing purposes and wait for approval.
|
||||||
- Load Sample
|
8. When working with MUI components, use the latest stable version and leverage the tools from the MCP server (`mui-mcp`).
|
||||||
- Format JSON
|
9. Do not hardcode color values. Use MUI theme palette colors instead.
|
||||||
- Clear All
|
10. Do not use emojis in code comments, program output, or log messages.
|
||||||
- Input area for JMESPath expressions
|
11. Suggest code commits, but never create them without consent.
|
||||||
- Message area for errors related to JMESPath expression syntax
|
12. Never push changes.
|
||||||
- Lower Middle left section: Input area for JSON data
|
|
||||||
- Lower Middle right section: Output are for JMESPath query results
|
|
||||||
- Bottom section: Footer with author and license information
|
|
||||||
|
|
||||||
The Middle section also contains a toolbar with buttons to load data from disk, load sample data, format JSON input, and clear all inputs.
|
|
||||||
|
|
||||||
The second page of the application contains:
|
|
||||||
|
|
||||||
- Top section: that is the same as the main page
|
|
||||||
- Middle section:
|
|
||||||
- API key display area with a button to regenerate the API key. The API key is 32 characters long cryptograghically secure random string.
|
|
||||||
- Instructions on how to use the API to upload sample data remotely with a code block displaying example curl command.
|
|
||||||
- Bottom section: Footer with author and license information.
|
|
||||||
|
|
||||||
The sample code block:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -s -X POST \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "Accept: application/json" \
|
|
||||||
-H "X-API-Key: {{API_KEY}}" \
|
|
||||||
--data @{{JSON_FILE_NAME}} \
|
|
||||||
"{{API_URL}}/api/v1/upload"
|
|
||||||
```
|
|
||||||
|
|
||||||
Placeholders `{{API_KEY}}` and `{{API_URL}}` should be replaced with the actual API key and the URL of the deployed application respectively. The `{{JSON_FILE_NAME}}` placeholder should be shown as is to indicate the file containing the JSON data to be uploaded.
|
|
||||||
|
|
||||||
The server code is only used as a bridge between the UI app and the external tools that may upload the sample data. The server does not perform any JMESPath evaluation or JSON parsing; all such logic is handled in the React application.
|
|
||||||
|
|
||||||
The server exposes a REST API to allow external tools to upload sample data that users can load into the application. The API key is required to upload sample data.
|
|
||||||
|
|
||||||
The API key is used for:
|
|
||||||
|
|
||||||
- encrypting the sample data
|
|
||||||
- authenticating download requests
|
|
||||||
|
|
||||||
Session id is a hash of the API key.
|
|
||||||
|
|
||||||
The server keeps two pieces of information in memory for each session:
|
|
||||||
|
|
||||||
1. The sample data itself.
|
|
||||||
2. A state variable (a GUID) that changes whenever new sample data is uploaded.
|
|
||||||
|
|
||||||
The maximum number of sessions to keep in memory set at the server startup using `MAX_SESSIONS` environment variable that defaults to 100. The maximum size of the sample data is set using `MAX_SAMPLE_SIZE` environment variable that defaults to 1 MB. Maximum session age is controlled using `MAX_SESSION_TTL` environment variable that defaults to 1 hour. After reaching the maximum number of sessions, the server rejects new uploads until some sessions expire. Sessions older than the maximum session age are automatically purged.
|
|
||||||
|
|
||||||
The UI generates an API key at startup then load the sample data at startup and periodically checks the state variable to see if new sample data is available. If state variable changes, the React app displays a button beneath the expression input area to reload the sample data. The reload is performed only when the user clicks the button.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
The main components of the application are located in the `src` directory and target Node 24 LTS environment.
|
|
||||||
|
|
||||||
Framework to be used:
|
|
||||||
|
|
||||||
- React for building the user interface.
|
|
||||||
- JavaScript (ES6+) for scripting.
|
|
||||||
- Bootstrap for styling and layout.
|
|
||||||
- Express.js for serving the application and handling API requests.
|
|
||||||
|
|
||||||
### API
|
|
||||||
|
|
||||||
The application exposes a REST API for remotly uploading sample data. The API endpoints are as follows:
|
|
||||||
|
|
||||||
- `POST /api/v1/upload`: The sample data is sent in the request body as JSON. The request must include an `x-api-key` header with the API key. If the upload is successful, the server responds with status 200 OK.
|
|
||||||
|
|
||||||
The server stores the sample data in memory and generates a new value for its state variable (a guid).
|
|
||||||
|
|
||||||
- `GET /api/v1/sample`: Returns the currently stored sample data as JSON. The request must include an `x-api-key` header with the API key. If the API key is invalid or the header is missing, the server responds with status 403 Forbidden.
|
|
||||||
|
|
||||||
- `GET /api/v1/state`: Returns the current value of the state variable (a guid) as a string. The request must include an `x-api-key` header with the API key. If the API key is invalid or the header is missing, the server responds with status 403 Forbidden.
|
|
||||||
|
|
||||||
## Containerization
|
|
||||||
|
|
||||||
The application should be prepared for deployment using containerization. It should extend minimal Node 24 LTS container image.
|
|
||||||
|
|
||||||
## Updates
|
|
||||||
|
|
||||||
Always use `scripts/new-version.js` script to make a new release.
|
|
||||||
|
|
||||||
Correct procedure to make a new release:
|
|
||||||
|
|
||||||
- Review the code changes and ensure everything is working.
|
|
||||||
- Run `npm run build` to build the React application.
|
|
||||||
- Run `npm test` to execute the test suite and ensure all tests pass.
|
|
||||||
- Prepare a commit message describing the changes made.
|
|
||||||
- Use `scripts/new-version.js` to create a new version and commit the changes. Use `--force` option if repository is not clean.
|
|
||||||
- Don't push the changes without approval.
|
|
||||||
- Don't build docker image without approval.
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
800
package-lock.json
generated
800
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -1,30 +1,34 @@
|
|||||||
{
|
{
|
||||||
"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": {
|
||||||
"start": "vite",
|
"start": "vite",
|
||||||
"prebuild": "node scripts/version-check.js",
|
"prebuild": "node scripts/version.mjs",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"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": "vite build && 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",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"semver": "^7.7.3",
|
||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
const { execSync } = require('child_process');
|
const { execSync } = require('child_process');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { pathToFileURL } = require('url');
|
||||||
const { parseArgs } = require('util');
|
const { parseArgs } = require('util');
|
||||||
|
|
||||||
function execCommand(command, description) {
|
function execCommand(command, description) {
|
||||||
@@ -31,20 +33,27 @@ function getContainerTool() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getVersion() {
|
async function generateVersionFile() {
|
||||||
try {
|
const versionModuleUrl = pathToFileURL(path.join(__dirname, 'version.mjs')).href;
|
||||||
// Try to get version from git tag
|
const { generateVersionFile: generate } = await import(versionModuleUrl);
|
||||||
const gitTag = execSync('git tag --points-at HEAD', { encoding: 'utf8' }).trim();
|
const versionFilePath = path.join(__dirname, '..', 'src', 'version.js');
|
||||||
if (gitTag) {
|
generate(versionFilePath);
|
||||||
return { version: gitTag.replace(/^v/, ''), isRelease: true };
|
return versionFilePath;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
// Git command failed, ignore
|
function readVersionFile(versionFilePath) {
|
||||||
|
const contents = fs.readFileSync(versionFilePath, 'utf8');
|
||||||
|
const versionMatch = contents.match(/export const VERSION = '([^']+)';/);
|
||||||
|
const releaseMatch = contents.match(/export const IS_RELEASE = (true|false);/);
|
||||||
|
|
||||||
|
if (!versionMatch || !releaseMatch) {
|
||||||
|
throw new Error(`Could not parse version file at ${versionFilePath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Development build - use package.json version with -dev suffix
|
return {
|
||||||
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
version: versionMatch[1],
|
||||||
return { version: `${packageJson.version}-dev`, isRelease: false };
|
isRelease: releaseMatch[1] === 'true'
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function getHostArchitecture() {
|
function getHostArchitecture() {
|
||||||
@@ -77,7 +86,7 @@ Examples:
|
|||||||
build-image.js -h # Show help`);
|
build-image.js -h # Show help`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function main() {
|
async function main() {
|
||||||
const { values } = parseArgs({
|
const { values } = parseArgs({
|
||||||
options: {
|
options: {
|
||||||
help: {
|
help: {
|
||||||
@@ -105,7 +114,8 @@ function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const containerTool = getContainerTool();
|
const containerTool = getContainerTool();
|
||||||
const { version, isRelease } = getVersion();
|
const versionFilePath = await generateVersionFile();
|
||||||
|
const { version, isRelease } = readVersionFile(versionFilePath);
|
||||||
|
|
||||||
let architectures;
|
let architectures;
|
||||||
if (values['all-arch']) {
|
if (values['all-arch']) {
|
||||||
@@ -160,5 +170,8 @@ function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (require.main === module) {
|
if (require.main === module) {
|
||||||
main();
|
main().catch((error) => {
|
||||||
|
console.error(`Error: ${error.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JMESPath Playground Upload Script (JavaScript)
|
|
||||||
* Usage: node upload.js [-u URL] [-k API_KEY] "json_file.json"
|
|
||||||
*/
|
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const https = require('https');
|
|
||||||
const http = require('http');
|
|
||||||
const { URL } = require('url');
|
|
||||||
const { parseArgs } = require('util');
|
|
||||||
|
|
||||||
function showUsage() {
|
|
||||||
const scriptName = path.basename(process.argv[1]);
|
|
||||||
console.log(`Usage: node ${scriptName} [-u|--url URL] [-k|--key API_KEY] <json_file>`);
|
|
||||||
console.log('');
|
|
||||||
console.log('Options:');
|
|
||||||
console.log(' -u, --url URL API URL (default: http://localhost:3000)');
|
|
||||||
console.log(' -k, --key API_KEY API key (not required for localhost)');
|
|
||||||
console.log(' -h, --help Show this help message');
|
|
||||||
console.log('');
|
|
||||||
console.log('Examples:');
|
|
||||||
console.log(` node ${scriptName} data.json`);
|
|
||||||
console.log(` node ${scriptName} -u http://example.com:3000 -k your-api-key data.json`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getArguments() {
|
|
||||||
const { values, positionals } = parseArgs({
|
|
||||||
args: process.argv.slice(2),
|
|
||||||
options: {
|
|
||||||
url: { type: 'string', short: 'u', default: 'http://localhost:3000' },
|
|
||||||
key: { type: 'string', short: 'k' },
|
|
||||||
help: { type: 'boolean', short: 'h' }
|
|
||||||
},
|
|
||||||
allowPositionals: true
|
|
||||||
});
|
|
||||||
|
|
||||||
if (values.help) {
|
|
||||||
showUsage();
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (positionals.length !== 1) {
|
|
||||||
console.error('Error: JSON file required');
|
|
||||||
showUsage();
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
apiUrl: values.url,
|
|
||||||
apiKey: values.key || '',
|
|
||||||
jsonFile: positionals[0]
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function validateJsonFile(jsonFile) {
|
|
||||||
// Check if file exists
|
|
||||||
if (!fs.existsSync(jsonFile)) {
|
|
||||||
console.error(`Error: JSON file '${jsonFile}' not found`);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate JSON content
|
|
||||||
try {
|
|
||||||
const content = fs.readFileSync(jsonFile, 'utf8');
|
|
||||||
JSON.parse(content);
|
|
||||||
return content;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error: '${jsonFile}' contains invalid JSON`);
|
|
||||||
console.error(error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isLocalhost(url) {
|
|
||||||
try {
|
|
||||||
const parsed = new URL(url);
|
|
||||||
const hostname = parsed.hostname;
|
|
||||||
return hostname === 'localhost' ||
|
|
||||||
hostname === '127.0.0.1' ||
|
|
||||||
hostname.startsWith('127.') ||
|
|
||||||
hostname === '::1';
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeRequest(url, options) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const parsedUrl = new URL(url);
|
|
||||||
const isHttps = parsedUrl.protocol === 'https:';
|
|
||||||
const client = isHttps ? https : http;
|
|
||||||
|
|
||||||
const requestOptions = {
|
|
||||||
hostname: parsedUrl.hostname,
|
|
||||||
port: parsedUrl.port,
|
|
||||||
path: parsedUrl.pathname,
|
|
||||||
method: options.method || 'GET',
|
|
||||||
headers: options.headers || {}
|
|
||||||
};
|
|
||||||
|
|
||||||
const req = client.request(requestOptions, (res) => {
|
|
||||||
let data = '';
|
|
||||||
res.on('data', chunk => data += chunk);
|
|
||||||
res.on('end', () => {
|
|
||||||
resolve({
|
|
||||||
ok: res.statusCode >= 200 && res.statusCode < 300,
|
|
||||||
status: res.statusCode,
|
|
||||||
json: () => Promise.resolve(JSON.parse(data))
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
req.on('error', reject);
|
|
||||||
|
|
||||||
if (options.body) {
|
|
||||||
req.write(options.body);
|
|
||||||
}
|
|
||||||
|
|
||||||
req.end();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function uploadData(apiUrl, apiKey, jsonFile, jsonData) {
|
|
||||||
try {
|
|
||||||
const headers = {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
};
|
|
||||||
|
|
||||||
// Only send API key for non-localhost requests
|
|
||||||
const isLocal = isLocalhost(apiUrl);
|
|
||||||
if (!isLocal && apiKey) {
|
|
||||||
headers['X-API-Key'] = apiKey;
|
|
||||||
} else if (!isLocal && !apiKey) {
|
|
||||||
console.error('Error: API key required for non-localhost URLs');
|
|
||||||
console.error('Use -k/--key option to specify API key');
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await makeRequest(`${apiUrl}/api/v1/upload`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: headers,
|
|
||||||
body: jsonData
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
const errorData = await response.json().catch(() => ({}));
|
|
||||||
throw new Error(`HTTP ${response.status}: ${errorData.error || 'Upload failed'}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await response.json();
|
|
||||||
console.log(JSON.stringify(result));
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error uploading data:', error.message);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const { apiUrl, apiKey, jsonFile } = getArguments();
|
|
||||||
const jsonData = await validateJsonFile(jsonFile);
|
|
||||||
await uploadData(apiUrl, apiKey, jsonFile, jsonData);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run the script
|
|
||||||
main().catch((error) => {
|
|
||||||
console.error('Unexpected error:', error);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
const fs = require('fs');
|
|
||||||
const path = require('path');
|
|
||||||
const { execSync } = require('child_process');
|
|
||||||
|
|
||||||
// Read package.json for base version
|
|
||||||
const packagePath = './package.json';
|
|
||||||
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
||||||
|
|
||||||
let version = pkg.version;
|
|
||||||
let isRelease = false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Check if current commit is tagged
|
|
||||||
const gitTag = execSync('git tag --points-at HEAD', { encoding: 'utf8' }).trim();
|
|
||||||
|
|
||||||
if (gitTag) {
|
|
||||||
// We're at a tagged commit - extract version from tag
|
|
||||||
const tagVersion = gitTag.replace(/^v/, ''); // Remove 'v' prefix if present
|
|
||||||
version = tagVersion;
|
|
||||||
console.log(`✅ Building release version ${version} (tagged: ${gitTag})`);
|
|
||||||
isRelease = true;
|
|
||||||
} else {
|
|
||||||
// We're not at a tagged commit - use unknown version
|
|
||||||
version = 'unknown';
|
|
||||||
console.log(`📦 Building development version with unknown version`);
|
|
||||||
isRelease = false;
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
// Git command failed (maybe not in a git repo)
|
|
||||||
version = 'unknown';
|
|
||||||
console.log(`⚠️ Cannot determine git status, using unknown version`);
|
|
||||||
isRelease = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate version.js file
|
|
||||||
const versionFile = path.join('./src', 'version.js');
|
|
||||||
const versionContent = `// Auto-generated version file - do not edit manually
|
|
||||||
// Generated at: ${new Date().toISOString()}
|
|
||||||
|
|
||||||
export const VERSION = '${version}';
|
|
||||||
export const IS_RELEASE = ${isRelease};
|
|
||||||
export const BUILD_TIME = '${new Date().toISOString()}';
|
|
||||||
`;
|
|
||||||
|
|
||||||
fs.writeFileSync(versionFile, versionContent);
|
|
||||||
console.log(`📝 Generated ${versionFile} with version ${version}`);
|
|
||||||
69
scripts/version.mjs
Normal file
69
scripts/version.mjs
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { readFileSync, write, writeFileSync } from "fs";
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
import semver from "semver";
|
||||||
|
|
||||||
|
export function getGitVersion() {
|
||||||
|
let rawGitVersion;
|
||||||
|
let gitVersion;
|
||||||
|
|
||||||
|
try {
|
||||||
|
rawGitVersion = execSync("git describe --tags --dirty").toString().trim();
|
||||||
|
gitVersion = semver.coerce(rawGitVersion) || semver.coerce("0.0.0");
|
||||||
|
} catch (e) {
|
||||||
|
return "0.0.0";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Git describe may return versions like v1.2.3-4-gabcdef
|
||||||
|
// or v1.2.3-dirty or v1.2.3 or v1.2.3-4-gabcdef-dirty.
|
||||||
|
// We need to return either a clean version or
|
||||||
|
// append -dev for modified versions and
|
||||||
|
// -dirty for dirty working tree.
|
||||||
|
if (rawGitVersion.endsWith("-dirty")) {
|
||||||
|
return gitVersion.version + "-dirty";
|
||||||
|
} else if (rawGitVersion.includes("-")) {
|
||||||
|
return gitVersion.version + "-dev";
|
||||||
|
} else {
|
||||||
|
return gitVersion.version || "0.0.0";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function generateVersionFile(versionFilePath) {
|
||||||
|
// Read package.json version
|
||||||
|
const packageVersion = JSON.parse(
|
||||||
|
readFileSync("package.json", { encoding: "utf-8" }),
|
||||||
|
).version;
|
||||||
|
// Get version from git repository
|
||||||
|
const gitVersion = getGitVersion();
|
||||||
|
const gitBaseVersion = semver.coerce(gitVersion)?.version;
|
||||||
|
|
||||||
|
// if git returned malformed version, throw error
|
||||||
|
if (!gitBaseVersion || gitBaseVersion === "0.0.0") {
|
||||||
|
throw new Error(
|
||||||
|
"Cannot determine git version. Make sure the script is run in a git repository with tags.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare git version with package.json version
|
||||||
|
if (semver.neq(gitBaseVersion, packageVersion)) {
|
||||||
|
throw new Error(
|
||||||
|
`Version mismatch: package.json version is ${packageVersion}, but git version is ${gitBaseVersion}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate version file
|
||||||
|
const buildDate = new Date().toISOString();
|
||||||
|
writeFileSync(
|
||||||
|
versionFilePath,
|
||||||
|
`// Auto-generated version file - do not edit manually
|
||||||
|
// Generated at: ${buildDate}
|
||||||
|
|
||||||
|
export const VERSION = '${packageVersion}';
|
||||||
|
export const IS_RELEASE = ${gitVersion === packageVersion};
|
||||||
|
export const BUILD_TIME = '${buildDate}';
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||||
|
generateVersionFile("src/version.js");
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
84
src/App.jsx
84
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,14 +148,32 @@ 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">
|
<>
|
||||||
|
<CssBaseline />
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
height: "100vh",
|
||||||
|
overflow: "hidden",
|
||||||
|
bgcolor: "background.default",
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Header
|
<Header
|
||||||
theme={theme}
|
theme={theme}
|
||||||
onThemeChange={handleThemeChange}
|
onThemeChange={handleThemeChange}
|
||||||
@@ -181,9 +182,15 @@ function App() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Main Content Section - flex-grow to fill space */}
|
{/* Main Content Section - flex-grow to fill space */}
|
||||||
<div
|
<Box
|
||||||
className="container-fluid flex-grow-1 d-flex flex-column"
|
component="main"
|
||||||
style={{ minHeight: 0 }}
|
sx={{
|
||||||
|
flexGrow: 1,
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
minHeight: 0,
|
||||||
|
height: "100%", // Force height for children
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{currentPage === "main" ? (
|
{currentPage === "main" ? (
|
||||||
<MainPage
|
<MainPage
|
||||||
@@ -200,13 +207,14 @@ function App() {
|
|||||||
apiKey={apiKey}
|
apiKey={apiKey}
|
||||||
onRegenerateApiKey={regenerateApiKey}
|
onRegenerateApiKey={regenerateApiKey}
|
||||||
shellType={shellType}
|
shellType={shellType}
|
||||||
onShellTypeChange={setShellType}
|
onShellTypeChange={handleShellTypeChange}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Box>
|
||||||
|
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</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,107 +86,119 @@ 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>
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<div className="mb-4">
|
<Box sx={{ mb: 6 }}>
|
||||||
<div className="d-flex justify-content-between align-items-center mb-2">
|
<Typography variant="subtitle2" gutterBottom color="text.secondary">
|
||||||
<h6 className="mb-0">📡 Remote Data Upload API</h6>
|
YOUR API KEY
|
||||||
<div className="btn-group btn-group-sm" role="group" aria-label="Shell type selector">
|
</Typography>
|
||||||
<input
|
<Box sx={{ display: "flex", gap: 1.5, alignItems: "center" }}>
|
||||||
type="radio"
|
<TextField
|
||||||
className="btn-check"
|
fullWidth
|
||||||
name="shellType"
|
value={apiKey}
|
||||||
id="shellBash"
|
slotProps={{
|
||||||
autoComplete="off"
|
input: {
|
||||||
checked={shellType === 'bash'}
|
readOnly: true,
|
||||||
onChange={() => onShellTypeChange('bash')}
|
style: { fontFamily: "'Noto Sans Mono', monospace", fontSize: "0.9rem" },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
variant="outlined"
|
||||||
|
sx={{ "& .MuiOutlinedInput-root": { bgcolor: "background.paper" } }}
|
||||||
/>
|
/>
|
||||||
<label className="btn btn-outline-secondary" htmlFor="shellBash">UNIX (Bash)</label>
|
<Tooltip title="Copy API Key">
|
||||||
<input
|
<IconButton
|
||||||
type="radio"
|
onClick={handleCopyToClipboard}
|
||||||
className="btn-check"
|
color={copySuccess ? "success" : "primary"}
|
||||||
name="shellType"
|
size="medium"
|
||||||
id="shellPowerShell"
|
sx={{ border: 1, borderColor: "divider" }}
|
||||||
autoComplete="off"
|
>
|
||||||
checked={shellType === 'powershell'}
|
{copySuccess ? <CheckIcon /> : <ContentCopyIcon />}
|
||||||
onChange={() => onShellTypeChange('powershell')}
|
</IconButton>
|
||||||
/>
|
</Tooltip>
|
||||||
<label className="btn btn-outline-secondary" htmlFor="shellPowerShell">Windows (PowerShell)</label>
|
<Tooltip title="Regenerate Key">
|
||||||
</div>
|
<IconButton
|
||||||
</div>
|
onClick={onRegenerateApiKey}
|
||||||
<p className="text-muted">
|
color="primary"
|
||||||
External tools can upload sample data remotely using the REST API.
|
size="medium"
|
||||||
The API key is required for authentication. Define two
|
sx={{ border: 1, borderColor: "divider" }}
|
||||||
environment variables in your {shellType === 'bash' ? <code>.bashrc</code> : <code>PowerShell profile</code>}.
|
>
|
||||||
</p>
|
<AutorenewIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
<Typography variant="caption" color="text.secondary" sx={{ mt: 1.5, display: "block" }}>
|
||||||
|
This key is stored locally in your browser. Use it to authenticate remote data uploads.
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Divider sx={{ my: 4, borderColor: "divider" }} />
|
||||||
|
|
||||||
|
<Box sx={{ mb: 4 }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
mb: 3,
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: 2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography variant="h6" fontWeight="600" color="text.primary">Remote Upload API</Typography>
|
||||||
|
|
||||||
|
<ToggleButtonGroup
|
||||||
|
size="small"
|
||||||
|
value={shellType}
|
||||||
|
exclusive
|
||||||
|
onChange={(e, value) => value && onShellTypeChange(value)}
|
||||||
|
aria-label="shell type"
|
||||||
|
sx={{ "& .MuiToggleButton-root": { px: 2, py: 0.5 } }}
|
||||||
|
>
|
||||||
|
<ToggleButton value="bash">UNIX (Bash)</ToggleButton>
|
||||||
|
<ToggleButton value="powershell">Windows (PS)</ToggleButton>
|
||||||
|
</ToggleButtonGroup>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Typography variant="body2" color="text.secondary" paragraph>
|
||||||
|
Use this endpoint to upload data from external scripts. Set these environment variables:
|
||||||
|
</Typography>
|
||||||
|
|
||||||
<CodeBlock
|
<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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,71 +1,88 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Typography,
|
||||||
|
Button,
|
||||||
|
ToggleButton,
|
||||||
|
ToggleButtonGroup,
|
||||||
|
Tooltip,
|
||||||
|
AppBar,
|
||||||
|
Toolbar,
|
||||||
|
Container,
|
||||||
|
Divider,
|
||||||
|
} from "@mui/material";
|
||||||
|
import KeyIcon from "@mui/icons-material/Key";
|
||||||
|
import HomeIcon from "@mui/icons-material/Home";
|
||||||
|
import BrightnessAutoIcon from "@mui/icons-material/BrightnessAuto";
|
||||||
|
import LightModeIcon from "@mui/icons-material/LightMode";
|
||||||
|
import DarkModeIcon from "@mui/icons-material/DarkMode";
|
||||||
|
|
||||||
function Header({ theme, onThemeChange, currentPage, onPageChange }) {
|
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')}
|
|
||||||
title="API Key Management"
|
|
||||||
>
|
>
|
||||||
🔐 API Keys
|
JMESPath Playground
|
||||||
</button>
|
</Typography>
|
||||||
{/* Theme switcher with theme-aware classes */}
|
</Box>
|
||||||
<div className="btn-group btn-group-sm" role="group" aria-label="Theme switcher">
|
|
||||||
<button
|
{/* Right side controls */}
|
||||||
type="button"
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||||
className={`btn ${
|
{/* API Key Management Button */}
|
||||||
theme === 'auto'
|
<Tooltip title={currentPage === "main" ? "API Key Management" : "Back to Testing"}>
|
||||||
? 'btn-light active'
|
<Button
|
||||||
: 'btn-outline-light'
|
variant={currentPage === "apikey" ? "contained" : "text"}
|
||||||
}`}
|
color={currentPage === "apikey" ? "primary" : "primary"}
|
||||||
onClick={() => onThemeChange('auto')}
|
size="medium"
|
||||||
title="Auto (follow system)"
|
startIcon={currentPage === "main" ? <KeyIcon /> : <HomeIcon />}
|
||||||
|
onClick={() => onPageChange(currentPage === "main" ? "apikey" : "main")}
|
||||||
>
|
>
|
||||||
🌓 Auto
|
{currentPage === "main" ? "API Keys" : "Home"}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
</Tooltip>
|
||||||
type="button"
|
|
||||||
className={`btn ${
|
<Divider orientation="vertical" flexItem sx={{ my: 2, mx: 1 }} />
|
||||||
theme === 'light'
|
|
||||||
? 'btn-light active'
|
{/* Theme switcher */}
|
||||||
: 'btn-outline-light'
|
<ToggleButtonGroup
|
||||||
}`}
|
value={theme}
|
||||||
onClick={() => onThemeChange('light')}
|
exclusive
|
||||||
title="Light theme"
|
onChange={(e, nextTheme) => nextTheme && onThemeChange(nextTheme)}
|
||||||
|
aria-label="theme switcher"
|
||||||
|
size="small"
|
||||||
>
|
>
|
||||||
☀️ Light
|
<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 === 'dark'
|
<Tooltip title="Light mode">
|
||||||
? 'btn-light active'
|
<ToggleButton value="light" aria-label="Light">
|
||||||
: 'btn-outline-light'
|
<LightModeIcon sx={{ fontSize: "1.2rem" }} />
|
||||||
}`}
|
</ToggleButton>
|
||||||
onClick={() => onThemeChange('dark')}
|
</Tooltip>
|
||||||
title="Dark theme"
|
<Tooltip title="Dark mode">
|
||||||
>
|
<ToggleButton value="dark" aria-label="Dark">
|
||||||
🌙 Dark
|
<DarkModeIcon sx={{ fontSize: "1.2rem" }} />
|
||||||
</button>
|
</ToggleButton>
|
||||||
</div>
|
</Tooltip>
|
||||||
</div>
|
</ToggleButtonGroup>
|
||||||
</div>
|
</Box>
|
||||||
</div>
|
</Toolbar>
|
||||||
</div>
|
</Container>
|
||||||
</div>
|
</AppBar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
overflow: "hidden",
|
||||||
|
mb: 2
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
px: 2,
|
||||||
|
py: 1,
|
||||||
|
bgcolor: "action.hover",
|
||||||
|
borderBottom: 1,
|
||||||
|
borderColor: "divider",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
|
||||||
|
<SearchIcon sx={{ fontSize: 20 }} color="primary" />
|
||||||
|
<Typography variant="subtitle2" color="text.primary">
|
||||||
JMESPath Expression
|
JMESPath Expression
|
||||||
</h6>
|
</Typography>
|
||||||
</div>
|
</Box>
|
||||||
<div className="card-body">
|
</Box>
|
||||||
<input
|
<Box sx={{ p: 1.5, mt: 0.5 }}>
|
||||||
type="text"
|
<TextField
|
||||||
className={`form-control jmespath-input ${error ? "error" : "success"}`}
|
fullWidth
|
||||||
|
size="small"
|
||||||
|
placeholder="Enter JMESPath expression (e.g., people[*].name)"
|
||||||
value={jmespathExpression}
|
value={jmespathExpression}
|
||||||
onChange={handleJmespathChange}
|
onChange={handleJmespathChange}
|
||||||
placeholder="Enter JMESPath expression (e.g., people[*].name)"
|
error={!!error}
|
||||||
/>
|
helperText={error || " "}
|
||||||
<div
|
sx={{
|
||||||
className={`alert mt-2 mb-0 d-flex justify-content-between align-items-center ${error ? "alert-danger" : "alert-success"}`}
|
"& .MuiInputBase-root": {
|
||||||
>
|
fontFamily: "'Noto Sans Mono', monospace",
|
||||||
<small className="mb-0">
|
fontSize: "0.9rem",
|
||||||
{error || "Expression is correct"}
|
},
|
||||||
</small>
|
"& .MuiFormHelperText-root": {
|
||||||
{showReloadButton && (
|
mt: 0.75,
|
||||||
<button
|
mb: -0.5,
|
||||||
className="btn btn-light btn-sm ms-2 border"
|
},
|
||||||
onClick={() => {
|
|
||||||
onReloadSampleData();
|
|
||||||
}}
|
}}
|
||||||
title="New sample data is available"
|
/>
|
||||||
>
|
</Box>
|
||||||
<i className="bi bi-arrow-clockwise me-1"></i>
|
</Paper>
|
||||||
Reload Sample Data
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 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"
|
}}
|
||||||
|
>
|
||||||
|
<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}
|
onClick={loadFromDisk}
|
||||||
title="Load JSON object from file"
|
color="primary"
|
||||||
|
aria-label="Load from Disk"
|
||||||
>
|
>
|
||||||
📄 Load an Object
|
<FileOpenIcon fontSize="small" />
|
||||||
</button>
|
</IconButton>
|
||||||
<button
|
</Tooltip>
|
||||||
className="btn btn-outline-info btn-sm me-2"
|
<Tooltip title="Load Logs">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
onClick={loadLogFile}
|
onClick={loadLogFile}
|
||||||
title="Load JSON Lines log file"
|
color="primary"
|
||||||
|
aria-label="Load Logs"
|
||||||
>
|
>
|
||||||
📋 Load a Log File
|
<UploadFileIcon fontSize="small" />
|
||||||
</button>
|
</IconButton>
|
||||||
<button
|
</Tooltip>
|
||||||
className="btn btn-outline-primary btn-sm me-2"
|
<Tooltip title="Load Sample">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
onClick={loadSample}
|
onClick={loadSample}
|
||||||
title="Load sample data"
|
color="primary"
|
||||||
|
aria-label="Load Sample"
|
||||||
>
|
>
|
||||||
Load Sample
|
<RestoreIcon fontSize="small" />
|
||||||
</button>
|
</IconButton>
|
||||||
<button
|
</Tooltip>
|
||||||
className="btn btn-outline-secondary btn-sm me-2"
|
<Tooltip title="Format">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
onClick={formatJson}
|
onClick={formatJson}
|
||||||
title="Format JSON"
|
color="primary"
|
||||||
|
aria-label="Format"
|
||||||
>
|
>
|
||||||
Format JSON
|
<FormatAlignLeftIcon fontSize="small" />
|
||||||
</button>
|
</IconButton>
|
||||||
<button
|
</Tooltip>
|
||||||
className="btn btn-outline-danger btn-sm"
|
<Divider orientation="vertical" flexItem sx={{ mx: 0.5 }} />
|
||||||
|
<Tooltip title="Clear all inputs">
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
onClick={clearAll}
|
onClick={clearAll}
|
||||||
title="Clear all inputs"
|
color="secondary"
|
||||||
|
aria-label="Clear all inputs"
|
||||||
>
|
>
|
||||||
Clear All
|
<ClearIcon fontSize="small" />
|
||||||
</button>
|
</IconButton>
|
||||||
</div>
|
</Tooltip>
|
||||||
</div>
|
</Stack>
|
||||||
<div
|
</Box>
|
||||||
className="card-body flex-grow-1 d-flex flex-column"
|
<Box sx={{ p: 2, flex: "1 1 0", display: "flex", flexDirection: "column", minHeight: 0, overflow: "hidden" }}>
|
||||||
style={{ minHeight: 0 }}
|
<TextField
|
||||||
>
|
multiline
|
||||||
<textarea
|
fullWidth
|
||||||
className={`form-control json-input flex-grow-1 ${jsonError ? "error" : "success"}`}
|
|
||||||
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,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<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}
|
onClick={copyToClipboard}
|
||||||
disabled={!result || result === "null"}
|
disabled={!result || result === "null"}
|
||||||
title="Copy result to clipboard"
|
color={copySuccess ? "success" : "primary"}
|
||||||
>
|
>
|
||||||
<i className={`bi ${copySuccess ? "bi-check-lg" : "bi-clipboard"} me-1`}></i>
|
{copySuccess ? <CheckIcon fontSize="small" /> : <ContentCopyIcon fontSize="small" />}
|
||||||
{copySuccess ? "Copied!" : "Copy"}
|
</IconButton>
|
||||||
</button>
|
</span>
|
||||||
<button
|
</Tooltip>
|
||||||
className="btn btn-outline-secondary btn-sm"
|
<Tooltip title="Download Result">
|
||||||
|
<span>
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
onClick={downloadResult}
|
onClick={downloadResult}
|
||||||
disabled={!result || result === "null"}
|
disabled={!result || result === "null"}
|
||||||
title="Download result as JSON file"
|
color="primary"
|
||||||
>
|
>
|
||||||
<i className="bi bi-download me-1"></i>
|
<DownloadIcon fontSize="small" />
|
||||||
Download
|
</IconButton>
|
||||||
</button>
|
</span>
|
||||||
</div>
|
</Tooltip>
|
||||||
</div>
|
</Stack>
|
||||||
<div
|
</Box>
|
||||||
className="card-body flex-grow-1 d-flex flex-column"
|
<Box sx={{ p: 2, flex: "1 1 0", display: "flex", flexDirection: "column", minHeight: 0, overflow: "hidden" }}>
|
||||||
style={{ minHeight: 0 }}
|
<TextField
|
||||||
>
|
multiline
|
||||||
<textarea
|
fullWidth
|
||||||
className="form-control result-output flex-grow-1"
|
|
||||||
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>
|
||||||
|
<ThemeProvider theme={theme} defaultMode="system">
|
||||||
<App />
|
<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