75062dbf5e
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
158 lines
6.0 KiB
React
158 lines
6.0 KiB
React
import { useState } from 'react';
|
|
import axios from '../services/api';
|
|
|
|
const sectionBox = {
|
|
marginTop: '14px',
|
|
padding: '14px',
|
|
background: 'var(--surface-2)',
|
|
border: '1px solid var(--border)',
|
|
borderRadius: '12px',
|
|
};
|
|
|
|
const EXAMPLES = [
|
|
{ label: '* * * * *', value: '* * * * *' },
|
|
{ label: '0 9 * * 1-5', value: '0 9 * * 1-5' },
|
|
{ label: '0 0 1 * *', value: '0 0 1 * *' },
|
|
{ label: '*/15 * * * *', value: '*/15 * * * *' },
|
|
];
|
|
|
|
const FIELD_LABELS = [
|
|
{ key: 'minute', label: 'Minute' },
|
|
{ key: 'hour', label: 'Stunde' },
|
|
{ key: 'day', label: 'Tag' },
|
|
{ key: 'month', label: 'Monat' },
|
|
{ key: 'weekday', label: 'Wochentag' },
|
|
];
|
|
|
|
function CronExplainerTool() {
|
|
const [expression, setExpression] = useState('');
|
|
const [result, setResult] = useState(null);
|
|
const [error, setError] = useState('');
|
|
|
|
const explain = async (expr) => {
|
|
const val = expr !== undefined ? expr : expression;
|
|
setError('');
|
|
setResult(null);
|
|
if (!val.trim()) {
|
|
setError('Bitte Cron-Ausdruck eingeben.');
|
|
return;
|
|
}
|
|
try {
|
|
const res = await axios.post('/api/cron/explain', { expression: val });
|
|
setResult(res.data);
|
|
} catch (err) {
|
|
setError(err.response?.data?.error || err.response?.data?.message || 'Ungültiger Cron-Ausdruck');
|
|
}
|
|
};
|
|
|
|
const useExample = (val) => {
|
|
setExpression(val);
|
|
setError('');
|
|
setResult(null);
|
|
explain(val);
|
|
};
|
|
|
|
const formatDate = (iso) => {
|
|
const d = new Date(iso);
|
|
return d.toLocaleString('de-DE', {
|
|
weekday: 'short', day: '2-digit', month: '2-digit',
|
|
year: 'numeric', hour: '2-digit', minute: '2-digit',
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div className="main-content">
|
|
<h2>Cron Erklärer</h2>
|
|
<p style={{ color: 'var(--muted)', marginBottom: '16px', fontSize: '0.95rem' }}>
|
|
Cron-Ausdrücke (5 Felder) analysieren und auf Deutsch erklären.
|
|
</p>
|
|
|
|
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', marginBottom: '10px' }}>
|
|
{EXAMPLES.map(({ label, value }) => (
|
|
<button
|
|
key={value}
|
|
className="ghost"
|
|
onClick={() => useExample(value)}
|
|
style={{ fontFamily: 'monospace', fontSize: '0.85rem' }}
|
|
>
|
|
{label}
|
|
</button>
|
|
))}
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', gap: '8px' }}>
|
|
<input
|
|
type="text"
|
|
value={expression}
|
|
onChange={(e) => { setExpression(e.target.value); setResult(null); setError(''); }}
|
|
placeholder="z.B. 0 9 * * 1-5"
|
|
style={{ fontFamily: 'monospace', flex: 1 }}
|
|
onKeyDown={(e) => e.key === 'Enter' && explain()}
|
|
/>
|
|
<button onClick={() => explain()} style={{ flexShrink: 0 }}>Erklären</button>
|
|
</div>
|
|
|
|
{error && <p className="error" style={{ marginTop: '12px' }}>{error}</p>}
|
|
|
|
{result && (
|
|
<>
|
|
<div style={{ ...sectionBox, borderColor: 'var(--accent)' }}>
|
|
<p style={{ color: 'var(--muted)', fontSize: '0.8rem', marginBottom: '4px', fontWeight: 600 }}>Erklärung</p>
|
|
<p style={{ color: 'var(--text)', fontWeight: 600, fontSize: '1.05rem' }}>{result.explanation}</p>
|
|
</div>
|
|
|
|
<div style={sectionBox}>
|
|
<p style={{ color: 'var(--muted)', fontSize: '0.8rem', marginBottom: '10px', fontWeight: 600 }}>Felder</p>
|
|
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
|
<thead>
|
|
<tr>
|
|
<th style={{ textAlign: 'left', padding: '6px 10px', color: 'var(--muted)', fontSize: '0.8rem', fontWeight: 600, borderBottom: '1px solid var(--border)' }}>Feld</th>
|
|
<th style={{ textAlign: 'left', padding: '6px 10px', color: 'var(--muted)', fontSize: '0.8rem', fontWeight: 600, borderBottom: '1px solid var(--border)' }}>Ausdruck</th>
|
|
<th style={{ textAlign: 'left', padding: '6px 10px', color: 'var(--muted)', fontSize: '0.8rem', fontWeight: 600, borderBottom: '1px solid var(--border)' }}>Bedeutung</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{FIELD_LABELS.map(({ key, label }, i) => (
|
|
<tr key={key} style={{ background: i % 2 === 0 ? 'transparent' : 'rgba(255,255,255,0.02)' }}>
|
|
<td style={{ padding: '8px 10px', color: 'var(--accent)', fontWeight: 600, fontSize: '0.9rem' }}>{label}</td>
|
|
<td style={{ padding: '8px 10px', fontFamily: 'monospace', color: 'var(--text)', fontSize: '0.9rem' }}>
|
|
{expression.split(/\s+/)[i] || '—'}
|
|
</td>
|
|
<td style={{ padding: '8px 10px', color: 'var(--text)', fontSize: '0.9rem' }}>
|
|
{result.fields[key]}
|
|
</td>
|
|
</tr>
|
|
))}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
|
|
{result.next_runs && result.next_runs.length > 0 && (
|
|
<div style={sectionBox}>
|
|
<p style={{ color: 'var(--muted)', fontSize: '0.8rem', marginBottom: '10px', fontWeight: 600 }}>Nächste 5 Ausführungen</p>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
|
|
{result.next_runs.map((run, i) => (
|
|
<div key={run} style={{
|
|
display: 'flex', alignItems: 'center', gap: '10px',
|
|
padding: '8px 12px',
|
|
background: i === 0 ? 'rgba(34,211,238,0.06)' : 'transparent',
|
|
borderRadius: '8px',
|
|
border: i === 0 ? '1px solid rgba(34,211,238,0.2)' : '1px solid transparent',
|
|
}}>
|
|
<span style={{ color: 'var(--muted)', width: '20px', fontSize: '0.85rem' }}>#{i + 1}</span>
|
|
<span style={{ fontFamily: 'monospace', color: i === 0 ? 'var(--accent)' : 'var(--text)', fontSize: '0.9rem' }}>
|
|
{formatDate(run)}
|
|
</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default CronExplainerTool;
|