1 Commits

8 changed files with 168 additions and 57 deletions

View File

@@ -1,15 +1,15 @@
#!/usr/bin/env pwsh #!/usr/bin/env pwsh
[CmdletBinding()] [CmdletBinding()]
param( param(
[Parameter(Position=0, HelpMessage='API base URL')] [Parameter(HelpMessage='Path to JSON file; default: read from stdin')]
[string]$JsonFile = '-',
[Parameter(HelpMessage='API base URL')]
[string]$ApiUrl, [string]$ApiUrl,
[Parameter(HelpMessage='API key for authentication')] [Parameter(HelpMessage='API key for authentication')]
[string]$ApiKey, [string]$ApiKey,
[Parameter(HelpMessage='Path to JSON file; default: read from stdin')]
[string]$JsonFile = '-',
[Parameter(HelpMessage='Show help')] [Parameter(HelpMessage='Show help')]
[switch]$Help [switch]$Help
) )

View File

@@ -1,6 +1,6 @@
{ {
"name": "jmespath-playground", "name": "jmespath-playground",
"version": "1.3.0", "version": "1.3.1",
"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": {
@@ -9,7 +9,7 @@
"build": "vite build", "build": "vite build",
"preview": "vite preview", "preview": "vite preview",
"test": "vitest", "test": "vitest",
"server": "node server.js", "server": "node server.js --dev",
"dev": "concurrently \"npm start\" \"npm run server\"", "dev": "concurrently \"npm start\" \"npm run server\"",
"build-image": "node scripts/build-image.js" "build-image": "node scripts/build-image.js"
}, },

View File

@@ -5,6 +5,22 @@
--font-mono: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace; --font-mono: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
--accent-color: #007bff; --accent-color: #007bff;
/* 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 */ /* Button variants */
--btn-success: #28a745; --btn-success: #28a745;
--btn-info: #17a2b8; --btn-info: #17a2b8;
@@ -36,23 +52,57 @@ body {
/* Header section styling - more compact */ /* Header section styling - more compact */
.header-section { .header-section {
/* Removed gradient background to fix text visibility */ 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 {
color: var(--brand-white);
}
/* Ensure buttons in header are clearly visible against gradient */
.header-section .btn-light.active {
background-color: var(--brand-white);
color: var(--brand-dark) !important; /* Deep dark text for selected states */
border-color: var(--brand-white);
}
.header-section .btn-outline-light {
color: var(--brand-white);
border-color: var(--brand-white-60);
}
.header-section .btn-outline-light:hover {
background-color: var(--brand-white-10);
color: var(--brand-white);
}
.header-section .btn-outline-warning {
color: var(--brand-warning);
border-color: var(--brand-warning-50);
}
.header-section .btn-outline-warning:hover {
background-color: var(--brand-warning-10);
color: var(--brand-warning);
}
/* Custom card styling */ /* Custom card styling */
.card { .card {
border: none; border: none;
box-shadow: 0 2px 8px rgba(0,0,0,0.1); box-shadow: 0 2px 8px var(--shadow-light);
border-radius: 8px; border-radius: 8px;
transition: background-color 0.3s ease, box-shadow 0.3s ease; transition: background-color 0.3s ease, box-shadow 0.3s ease;
} }
.card-header { .card-header {
background-color: #f8f9fa; background-color: var(--bg-secondary);
border-bottom: 2px solid #dee2e6; border-bottom: 2px solid var(--border);
font-weight: 600; font-weight: 600;
color: #212529; color: var(--text-primary);
transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease; transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease;
} }
@@ -224,7 +274,7 @@ footer a:hover {
/* Focus states */ /* Focus states */
.jmespath-input:focus { .jmespath-input:focus {
border-color: var(--accent-color, #007bff); border-color: var(--accent-color, #007bff);
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); box-shadow: 0 0 0 0.2rem var(--focus-ring);
} }
.json-input:focus, .json-input:focus,
@@ -232,7 +282,7 @@ footer a:hover {
background-color: var(--bg-primary); background-color: var(--bg-primary);
border-color: var(--accent-color, #007bff); border-color: var(--accent-color, #007bff);
color: var(--text-secondary); color: var(--text-secondary);
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); box-shadow: 0 0 0 0.2rem var(--focus-ring);
} }
/* Placeholder colors */ /* Placeholder colors */
@@ -249,6 +299,12 @@ footer a:hover {
color: var(--error-text); color: var(--error-text);
} }
.alert-success {
background-color: var(--success-bg);
border-color: var(--success-border);
color: var(--success-text);
}
/* Code block styles */ /* Code block styles */
pre.bg-light { pre.bg-light {
background-color: var(--bg-secondary) !important; background-color: var(--bg-secondary) !important;

View File

@@ -31,6 +31,10 @@ function App() {
// Load theme from localStorage or default to 'auto' // Load theme from localStorage or default to 'auto'
return localStorage.getItem("theme") || "auto"; return localStorage.getItem("theme") || "auto";
}); });
const [shellType, setShellType] = useState(() => {
// Load shell type from localStorage or default to 'bash'
return localStorage.getItem("jmespath-shell-type") || "bash";
});
const [showReloadButton, setShowReloadButton] = useState(false); const [showReloadButton, setShowReloadButton] = useState(false);
const [currentStateGuid, setCurrentStateGuid] = useState(null); const [currentStateGuid, setCurrentStateGuid] = useState(null);
const [jmespathExpression, setJmespathExpression] = const [jmespathExpression, setJmespathExpression] =
@@ -81,6 +85,11 @@ function App() {
localStorage.setItem("theme", theme); localStorage.setItem("theme", theme);
}, [theme]); }, [theme]);
// Shell type management
useEffect(() => {
localStorage.setItem("jmespath-shell-type", shellType);
}, [shellType]);
// Get headers for API requests // Get headers for API requests
const getApiHeaders = () => { const getApiHeaders = () => {
return { return {
@@ -163,7 +172,7 @@ function App() {
}; };
return ( return (
<div className="container-fluid vh-100 d-flex flex-column"> <div className="vh-100 d-flex flex-column">
<Header <Header
theme={theme} theme={theme}
onThemeChange={handleThemeChange} onThemeChange={handleThemeChange}
@@ -187,7 +196,12 @@ function App() {
setJsonData={setJsonData} setJsonData={setJsonData}
/> />
) : ( ) : (
<ApiKeyPage apiKey={apiKey} onRegenerateApiKey={regenerateApiKey} /> <ApiKeyPage
apiKey={apiKey}
onRegenerateApiKey={regenerateApiKey}
shellType={shellType}
onShellTypeChange={setShellType}
/>
)} )}
</div> </div>

View File

@@ -1,6 +1,36 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
function ApiKeyPage({ apiKey, onRegenerateApiKey }) { function CodeBlock({ code }) {
const [copySuccess, setCopySuccess] = useState(false);
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(code);
setCopySuccess(true);
setTimeout(() => setCopySuccess(false), 2000);
} catch (err) {
console.error('Failed to copy to clipboard:', err);
}
};
return (
<div className="position-relative">
<pre className="bg-light p-3 pe-5 rounded border shadow-sm">
<code className="d-block" style={{ whiteSpace: 'pre-wrap' }}>{code}</code>
</pre>
<button
className={`btn btn-sm ${copySuccess ? 'btn-success' : 'btn-outline-secondary'} position-absolute top-0 end-0 m-2`}
onClick={handleCopy}
title="Copy code to clipboard"
style={{ opacity: 0.8 }}
>
{copySuccess ? '✓' : '📋'}
</button>
</div>
);
}
function ApiKeyPage({ apiKey, onRegenerateApiKey, shellType, onShellTypeChange }) {
const [copySuccess, setCopySuccess] = useState(false); const [copySuccess, setCopySuccess] = useState(false);
const handleCopyToClipboard = async () => { const handleCopyToClipboard = async () => {
@@ -60,28 +90,50 @@ function ApiKeyPage({ apiKey, onRegenerateApiKey }) {
</div> </div>
<div className="mb-4"> <div className="mb-4">
<h6>📡 Remote Data Upload API</h6> <div className="d-flex justify-content-between align-items-center mb-2">
<h6 className="mb-0">📡 Remote Data Upload API</h6>
<div className="btn-group btn-group-sm" role="group" aria-label="Shell type selector">
<input
type="radio"
className="btn-check"
name="shellType"
id="shellBash"
autoComplete="off"
checked={shellType === 'bash'}
onChange={() => onShellTypeChange('bash')}
/>
<label className="btn btn-outline-secondary" htmlFor="shellBash">UNIX (Bash)</label>
<input
type="radio"
className="btn-check"
name="shellType"
id="shellPowerShell"
autoComplete="off"
checked={shellType === 'powershell'}
onChange={() => onShellTypeChange('powershell')}
/>
<label className="btn btn-outline-secondary" htmlFor="shellPowerShell">Windows (PowerShell)</label>
</div>
</div>
<p className="text-muted"> <p className="text-muted">
External tools can upload sample data remotely using the REST API. External tools can upload sample data remotely using the REST API.
The API key is required for authentication. Define two The API key is required for authentication. Define two
environment variables in your <code>.bashrc</code>. environment variables in your {shellType === 'bash' ? <code>.bashrc</code> : <code>PowerShell profile</code>}.
</p> </p>
<pre className="bg-light p-3 rounded border"> <CodeBlock
<code>export JMESPATH_PLAYGROUND_API_URL={window.location.origin}<br/>export JMESPATH_PLAYGROUND_API_KEY={apiKey}</code> code={shellType === 'bash'
</pre> ? `export JMESPATH_PLAYGROUND_API_URL=${window.location.origin}\nexport JMESPATH_PLAYGROUND_API_KEY=${apiKey}`
<p className="text-muted">Then, use the following <code>curl</code> command to upload your data:</p> : `$env:JMESPATH_PLAYGROUND_API_URL = "${window.location.origin}"\n$env:JMESPATH_PLAYGROUND_API_KEY = "${apiKey}"`}
<pre className="bg-light p-3 rounded border"> />
<code>{`curl -s -X POST \\ <p className="text-muted">Then, use the following {shellType === 'bash' ? <code>curl</code> : <code>PowerShell</code>} command to upload your data:</p>
-H "Content-Type: application/json" \\ <CodeBlock
-H "Accept: application/json" \\ code={shellType === 'bash'
-H "X-API-Key: $JMESPATH_PLAYGROUND_API_KEY" \\ ? `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"`
--data @__JSON_FILE_NAME__ \\ : `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__`}
"$\{JMESPATH_PLAYGROUND_API_URL}/api/v1/upload"`}</code> />
</pre>
<div className="form-text"> <div className="form-text">
Replace <code>{"__JSON_FILE_NAME__"}</code> with the path to your Replace <code>{"__JSON_FILE_NAME__"}</code> with the path to your
JSON file containing the sample data. or use <code>-</code> to JSON file containing the sample data. {shellType === 'bash' && <span>or use <code>-</code> to read from standard input.</span>}
read from standard input.
</div> </div>
</div> </div>
</div> </div>

View File

@@ -2,19 +2,19 @@ import React from 'react';
function Header({ theme, onThemeChange, currentPage, onPageChange }) { function Header({ theme, onThemeChange, currentPage, onPageChange }) {
return ( return (
<div className="header-section py-2"> <div className="header-section">
<div className="container"> <div className="container-fluid px-4">
<div className="row"> <div className="row align-items-center">
<div className="col-12 text-center position-relative"> <div className="col-12 text-center position-relative">
<h2 className="mb-1">JMESPath Testing Tool</h2> <h2 className="mb-1">JMESPath Testing Tool</h2>
{/* Right side controls - better positioning */} {/* Right side controls - better positioning */}
<div className="position-absolute top-0 end-0 d-flex align-items-center gap-2"> <div className="position-absolute top-50 end-0 translate-middle-y d-flex align-items-center gap-2 me-4">
{/* API Key Management Button - more prominent */} {/* API Key Management Button - more prominent */}
<button <button
type="button" type="button"
className={`btn btn-sm ${ className={`btn btn-sm ${
currentPage === 'apikey' currentPage === 'apikey'
? 'btn-warning fw-bold' ? 'btn-warning fw-bold text-dark'
: 'btn-outline-warning' : 'btn-outline-warning'
}`} }`}
onClick={() => onPageChange(currentPage === 'main' ? 'apikey' : 'main')} onClick={() => onPageChange(currentPage === 'main' ? 'apikey' : 'main')}
@@ -28,8 +28,8 @@ function Header({ theme, onThemeChange, currentPage, onPageChange }) {
type="button" type="button"
className={`btn ${ className={`btn ${
theme === 'auto' theme === 'auto'
? 'btn-primary' ? 'btn-light active'
: 'btn-outline-secondary' : 'btn-outline-light'
}`} }`}
onClick={() => onThemeChange('auto')} onClick={() => onThemeChange('auto')}
title="Auto (follow system)" title="Auto (follow system)"
@@ -40,8 +40,8 @@ function Header({ theme, onThemeChange, currentPage, onPageChange }) {
type="button" type="button"
className={`btn ${ className={`btn ${
theme === 'light' theme === 'light'
? 'btn-primary' ? 'btn-light active'
: 'btn-outline-secondary' : 'btn-outline-light'
}`} }`}
onClick={() => onThemeChange('light')} onClick={() => onThemeChange('light')}
title="Light theme" title="Light theme"
@@ -52,8 +52,8 @@ function Header({ theme, onThemeChange, currentPage, onPageChange }) {
type="button" type="button"
className={`btn ${ className={`btn ${
theme === 'dark' theme === 'dark'
? 'btn-primary' ? 'btn-light active'
: 'btn-outline-secondary' : 'btn-outline-light'
}`} }`}
onClick={() => onThemeChange('dark')} onClick={() => onThemeChange('dark')}
title="Dark theme" title="Dark theme"

View File

@@ -8,12 +8,6 @@ code {
monospace; monospace;
} }
.container-fluid {
height: 100vh;
display: flex;
flex-direction: column;
}
.content-section { .content-section {
flex: 1; flex: 1;
min-height: 0; min-height: 0;
@@ -52,13 +46,6 @@ code {
color: var(--success-text-light); color: var(--success-text-light);
} }
.header-section {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 2rem 0;
margin-bottom: 2rem;
}
/* Dark mode support for error states */ /* Dark mode support for error states */
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
.error { .error {

View File

@@ -4,10 +4,12 @@ import react from '@vitejs/plugin-react';
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
server: { server: {
host: '0.0.0.0',
port: 5173, port: 5173,
strictPort: true,
proxy: { proxy: {
'/api': { '/api': {
target: 'http://localhost:3000', target: 'http://127.0.0.1:3000',
changeOrigin: true, changeOrigin: true,
}, },
}, },