Release v1.3.1: Added PowerShell support and fixed theme issues

This commit is contained in:
2026-01-31 11:06:25 +01:00
parent d398c34aa5
commit 57371feeb0
8 changed files with 168 additions and 57 deletions

View File

@@ -1,15 +1,15 @@
#!/usr/bin/env pwsh
[CmdletBinding()]
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,
[Parameter(HelpMessage='API key for authentication')]
[string]$ApiKey,
[Parameter(HelpMessage='Path to JSON file; default: read from stdin')]
[string]$JsonFile = '-',
[Parameter(HelpMessage='Show help')]
[switch]$Help
)

View File

@@ -1,6 +1,6 @@
{
"name": "jmespath-playground",
"version": "1.3.0",
"version": "1.3.1",
"description": "A React-based web application for testing JMESPath expressions against JSON data",
"main": "index.js",
"scripts": {
@@ -9,7 +9,7 @@
"build": "vite build",
"preview": "vite preview",
"test": "vitest",
"server": "node server.js",
"server": "node server.js --dev",
"dev": "concurrently \"npm start\" \"npm run server\"",
"build-image": "node scripts/build-image.js"
},

View File

@@ -5,6 +5,22 @@
--font-mono: 'Monaco', 'Menlo', 'Ubuntu Mono', 'Consolas', monospace;
--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 */
--btn-success: #28a745;
--btn-info: #17a2b8;
@@ -36,23 +52,57 @@ body {
/* Header section styling - more compact */
.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;
}
.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 */
.card {
border: none;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
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: #f8f9fa;
border-bottom: 2px solid #dee2e6;
background-color: var(--bg-secondary);
border-bottom: 2px solid var(--border);
font-weight: 600;
color: #212529;
color: var(--text-primary);
transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease;
}
@@ -224,7 +274,7 @@ footer a:hover {
/* Focus states */
.jmespath-input:focus {
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,
@@ -232,7 +282,7 @@ footer a:hover {
background-color: var(--bg-primary);
border-color: var(--accent-color, #007bff);
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 */
@@ -249,6 +299,12 @@ footer a:hover {
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;

View File

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

View File

@@ -1,6 +1,36 @@
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 handleCopyToClipboard = async () => {
@@ -60,28 +90,50 @@ function ApiKeyPage({ apiKey, onRegenerateApiKey }) {
</div>
<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">
External tools can upload sample data remotely using the REST API.
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>
<pre className="bg-light p-3 rounded border">
<code>export JMESPATH_PLAYGROUND_API_URL={window.location.origin}<br/>export JMESPATH_PLAYGROUND_API_KEY={apiKey}</code>
</pre>
<p className="text-muted">Then, use the following <code>curl</code> command to upload your data:</p>
<pre className="bg-light p-3 rounded border">
<code>{`curl -s -X POST \\
-H "Content-Type: application/json" \\
-H "Accept: application/json" \\
-H "X-API-Key: $JMESPATH_PLAYGROUND_API_KEY" \\
--data @__JSON_FILE_NAME__ \\
"$\{JMESPATH_PLAYGROUND_API_URL}/api/v1/upload"`}</code>
</pre>
<CodeBlock
code={shellType === 'bash'
? `export JMESPATH_PLAYGROUND_API_URL=${window.location.origin}\nexport JMESPATH_PLAYGROUND_API_KEY=${apiKey}`
: `$env:JMESPATH_PLAYGROUND_API_URL = "${window.location.origin}"\n$env:JMESPATH_PLAYGROUND_API_KEY = "${apiKey}"`}
/>
<p className="text-muted">Then, use the following {shellType === 'bash' ? <code>curl</code> : <code>PowerShell</code>} command to upload your data:</p>
<CodeBlock
code={shellType === 'bash'
? `curl -s -X POST \\\n -H "Content-Type: application/json" \\\n -H "Accept: application/json" \\\n -H "X-API-Key: $JMESPATH_PLAYGROUND_API_KEY" \\\n --data @__JSON_FILE_NAME__ \\\n "$JMESPATH_PLAYGROUND_API_URL/api/v1/upload"`
: `Invoke-RestMethod -Uri "$env:JMESPATH_PLAYGROUND_API_URL/api/v1/upload" \\\n -Method Post \\\n -ContentType "application/json" \\\n -Headers @{\n "X-API-Key" = $env:JMESPATH_PLAYGROUND_API_KEY\n "Accept" = "application/json"\n } \\\n -InFile __JSON_FILE_NAME__`}
/>
<div className="form-text">
Replace <code>{"__JSON_FILE_NAME__"}</code> with the path to your
JSON file containing the sample data. or use <code>-</code> to
read from standard input.
JSON file containing the sample data. {shellType === 'bash' && <span>or use <code>-</code> to read from standard input.</span>}
</div>
</div>
</div>

View File

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

View File

@@ -8,12 +8,6 @@ code {
monospace;
}
.container-fluid {
height: 100vh;
display: flex;
flex-direction: column;
}
.content-section {
flex: 1;
min-height: 0;
@@ -52,13 +46,6 @@ code {
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 */
@media (prefers-color-scheme: dark) {
.error {

View File

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