SevdanurGenc's picture
Upload 300 files
9438a57 verified
Raw History Blame Contribute Delete
15.2 kB
import { useEffect, useMemo, useState } from 'react';
import { api } from '../api';
import { useI18n, type MsgKey } from '../i18n';
import type { Health, TrackDetail, TrackRun, TrackSummary } from '../types';
import { modelName } from './ModelSelect';
const LANG_NAME: Record<string, string> = { python: 'Python', javascript: 'JavaScript', typescript: 'TypeScript', java: 'Java' };
/** Listedeki küçük eğilim çizgisi (mutasyon, yoksa kapsama). */
function Spark({ data }: { data: (number | null)[] }) {
const v = data.filter((x): x is number => x != null);
const w = 72, h = 22;
if (v.length < 2) return <svg className="trk-spark" width={w} height={h} aria-hidden />;
const pts = v.map((y, i) => `${(i * (w - 4)) / (v.length - 1) + 2},${h - 2 - (y / 100) * (h - 4)}`).join(' ');
const last = pts.split(' ').pop()!.split(',');
return (
<svg className="trk-spark" width={w} height={h} viewBox={`0 0 ${w} ${h}`} aria-hidden>
<polyline points={pts} />
<circle cx={last[0]} cy={last[1]} r={2.5} />
</svg>
);
}
/** Zaman içinde kapsama ve mutasyon: tek eksen (0–100 %), iki seri, üzerine gelince ipucu. */
function TrendChart({ runs, health }: { runs: TrackRun[]; health: Health | null }) {
const { t, pct, date } = useI18n();
const [hover, setHover] = useState<number | null>(null);
const done = runs.filter((r) => r.status === 'done' && r.total);
if (done.length < 2) return <p className="small muted">{t('trk.chart.one')}</p>;
const W = 720, H = 240, L = 40, R = 96, T = 14, B = 30;
const x = (i: number) => L + (i * (W - L - R)) / (done.length - 1);
const y = (v: number) => T + (1 - v / 100) * (H - T - B);
const series = [
{ key: 'coverage' as const, cls: 's1', label: t('trk.chart.cov') },
{ key: 'mutation' as const, cls: 's2', label: t('trk.chart.mut') },
];
const path = (k: 'coverage' | 'mutation') => {
let d = '';
done.forEach((r, i) => {
const v = r[k];
if (v == null) return;
d += `${d && done[i - 1]?.[k] != null ? 'L' : 'M'}${x(i).toFixed(1)},${y(v).toFixed(1)}`;
});
return d;
};
const onMove = (e: React.MouseEvent) => {
const rect = (e.currentTarget as SVGElement).getBoundingClientRect();
const px = ((e.clientX - rect.left) / rect.width) * W;
let best = 0;
done.forEach((_, i) => { if (Math.abs(x(i) - px) < Math.abs(x(best) - px)) best = i; });
setHover(best);
};
const h = hover != null ? done[hover] : null;
const lastIdx = done.length - 1;
return (
<div className="trk-chart">
<div className="legend">
{series.map((s) => <span key={s.key}><i className={`sw ${s.cls}`} />{s.label}</span>)}
</div>
<svg viewBox={`0 0 ${W} ${H}`} width="100%" role="img" aria-label={t('trk.chart')}
onMouseMove={onMove} onMouseLeave={() => setHover(null)}>
{[0, 25, 50, 75, 100].map((v) => (
<g key={v}>
<line x1={L} x2={W - R} y1={y(v)} y2={y(v)} className="grid" />
<text x={L - 6} y={y(v) + 4} className="tick" textAnchor="end">{v}</text>
</g>
))}
{done.map((r, i) => (i === 0 || i === lastIdx || done.length <= 8 || i % Math.ceil(done.length / 8) === 0) && (
<text key={r.job_id} x={x(i)} y={H - 10} className="tick" textAnchor="middle">{i + 1}</text>
))}
{hover != null && <line x1={x(hover)} x2={x(hover)} y1={T} y2={H - B} className="cross" />}
{series.map((s) => (
<g key={s.key} className={s.cls}>
<path d={path(s.key)} className="line" />
{done.map((r, i) => r[s.key] != null && (
<circle key={r.job_id} cx={x(i)} cy={y(r[s.key] as number)} r={hover === i ? 5 : 4} className="dot" />
))}
{done[lastIdx][s.key] != null && (
<text x={x(lastIdx) + 10} y={y(done[lastIdx][s.key] as number) + 4} className="dlabel">
{pct(done[lastIdx][s.key], 0)}</text>
)}
</g>
))}
</svg>
{h && (
<div className="trk-tip" style={{ left: `${Math.min(80, Math.max(20, (x(hover!) / W) * 100))}%` }}>
<strong>{t('trk.tip.run', { n: hover! + 1 })}</strong> · {date(h.created_at)}<br />
<code>{h.model ? modelName(h.model, health) : '—'}</code> · {t(`trk.origin.${h.origin}` as MsgKey)}<br />
{t('hist.tests')}: {h.passed}/{h.total} · {t('trk.chart.cov')}: {pct(h.coverage)} · {t('trk.chart.mut')}: {pct(h.mutation)}
</div>
)}
</div>
);
}
export default function TrackingPage({ health, initial }: { health: Health | null; initial?: string }) {
const { t, pct, date, lang } = useI18n();
const [list, setList] = useState<TrackSummary[]>([]);
const [loaded, setLoaded] = useState(false);
const [sel, setSel] = useState<string | null>(initial || null);
const [detail, setDetail] = useState<TrackDetail | null>(null);
const [q, setQ] = useState('');
const [fLang, setFLang] = useState('');
const [fSrc, setFSrc] = useState('');
const [sort, setSort] = useState<'recent' | 'runs' | 'name' | 'best'>('recent');
useEffect(() => {
api.tracking().then((l) => { setList(l); setLoaded(true); setSel((s) => s || l[0]?.key || null); }).catch(() => setLoaded(true));
}, []);
useEffect(() => {
if (!sel) return;
window.history.replaceState(null, '', `#/tracking/${sel}`);
api.trackingDetail(sel).then(setDetail).catch(() => setDetail(null));
}, [sel]);
const title = (g: { title: string; title_en: string }) => (lang === 'en' ? g.title_en || g.title : g.title);
const shown = useMemo(() => {
const f = list.filter((g) => (!fLang || g.language === fLang) && (!fSrc || g.source === fSrc)
&& (!q || `${title(g)} ${g.sample_id || ''}`.toLowerCase().includes(q.toLowerCase())));
const by: Record<string, (a: TrackSummary, b: TrackSummary) => number> = {
recent: (a, b) => b.last_at - a.last_at,
runs: (a, b) => b.runs - a.runs || b.last_at - a.last_at,
name: (a, b) => title(a).localeCompare(title(b), lang),
best: (a, b) => (b.best?.mutation ?? -1) - (a.best?.mutation ?? -1),
};
return [...f].sort(by[sort]);
}, [list, q, fLang, fSrc, sort, lang]); // eslint-disable-line react-hooks/exhaustive-deps
const dur = (s: number | null) => (s == null ? '—' : s < 120 ? `${Math.round(s)} ${lang === 'tr' ? 'sn' : 's'}` : `${(s / 60).toFixed(1)} ${lang === 'tr' ? 'dk' : 'min'}`);
const d = detail && detail.key === sel ? detail : null;
return (
<div className="page">
<h1>{t('trk.title')}</h1>
<p className="lead">{t('trk.lead')}</p>
{loaded && !list.length ? (
<div className="panel"><div className="panel-body"><div className="empty"><div><h3>{t('trk.emptyTitle')}</h3><p>{t('trk.emptyText')}</p></div></div></div></div>
) : (
<div className="grid-2 trk-grid">
<div className="panel">
<div className="panel-head"><h2>{t('trk.codes')}</h2><div className="spacer" /><span className="small muted">{shown.length}</span></div>
<div className="panel-body" style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
<input type="search" placeholder={t('trk.search')} value={q} onChange={(e) => setQ(e.target.value)} />
<div className="row" style={{ gap: 6 }}>
<select value={fLang} onChange={(e) => setFLang(e.target.value)} aria-label={t('auto.allLangs')}>
<option value="">{t('auto.allLangs')}</option>
{Object.entries(LANG_NAME).map(([k, v]) => <option key={k} value={k}>{v}</option>)}
</select>
<select value={fSrc} onChange={(e) => setFSrc(e.target.value)} aria-label={t('trk.src.all')}>
<option value="">{t('trk.src.all')}</option>
{(['sample', 'library', 'custom'] as const).map((s) => <option key={s} value={s}>{t(`trk.src.${s}`)}</option>)}
</select>
<select value={sort} onChange={(e) => setSort(e.target.value as typeof sort)} aria-label="sort">
{(['recent', 'runs', 'best', 'name'] as const).map((s) => <option key={s} value={s}>{t(`trk.sort.${s}`)}</option>)}
</select>
</div>
<div className="trk-list">
{shown.map((g) => (
<button key={g.key} className={`trk-row${g.key === sel ? ' on' : ''}`} onClick={() => setSel(g.key)}>
<span className="trk-row-main">
<span className="trk-row-title">{title(g)}</span>
<span className="small muted">{LANG_NAME[g.language]} · {t('trk.runs', { n: g.runs })} · {date(g.last_at)}</span>
</span>
<Spark data={g.trend} />
<span className="trk-row-best small">{g.best?.mutation != null ? pct(g.best.mutation, 0) : g.best?.coverage != null ? pct(g.best.coverage, 0) : '—'}
<span className="muted"> {t('trk.best')}</span></span>
</button>
))}
</div>
</div>
</div>
<div className="panel">
{!d ? (
<div className="panel-body"><div className="empty"><div><p>{t('trk.pick')}</p></div></div></div>
) : (<>
<div className="panel-head">
<h2>{title(d)}</h2>
<div className="spacer" />
{d.best && <a className="btn" href={`#/job/${d.best.job_id}`}>{t('trk.openBest')}</a>}
<a className="btn" href={`#/job/${d.runs[d.runs.length - 1].job_id}`}>{t('trk.openLast')}</a>
<a className="btn" href={`/api/tracking/${d.key}/runs.csv`}>{t('trk.csv')}</a>
</div>
<div className="panel-body" style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div className="small muted">
{LANG_NAME[d.language]} · {t(`kind.${d.kind}` as MsgKey)} · {t('trk.lines', { n: d.lines })} · {t(`trk.src.${d.source}`)}
{d.sample_id && <> · <code>{d.sample_id}</code></>}
</div>
<div className="trk-kpis">
{[
[String(d.stats.runs), t('trk.k.runs')],
[`${d.stats.green}/${d.stats.runs}`, t('trk.k.green')],
[pct(d.stats.best_coverage), t('trk.k.bestCov')],
[pct(d.stats.best_mutation), t('trk.k.bestMut')],
[dur(d.stats.avg_duration_s), t('trk.k.avgDur')],
].map(([v, k]) => <div key={k} className="trk-kpi"><b>{v}</b><span>{k}</span></div>)}
</div>
<div>
<h3 className="trk-h">{t('trk.chart')}</h3>
<TrendChart runs={d.runs} health={health} />
</div>
<div>
<h3 className="trk-h">{t('trk.byModel')}</h3>
<div style={{ overflowX: 'auto' }}>
<table className="data-table">
<thead><tr><th>{t('trk.col.model')}</th><th className="num">{t('trk.col.runs')}</th><th className="num">{t('trk.col.green')}</th>
<th className="num">{t('trk.col.avgCov')}</th><th className="num">{t('trk.col.avgMut')}</th><th className="num">{t('trk.col.bestMut')}</th>
<th className="num">{t('trk.col.avgDur')}</th></tr></thead>
<tbody>
{d.models.map((m) => (
<tr key={m.model}><td><code>{m.model === '—' ? '—' : modelName(m.model, health)}</code></td>
<td className="num">{m.runs}</td><td className="num">{m.green}</td><td className="num">{pct(m.avg_coverage)}</td>
<td className="num">{pct(m.avg_mutation)}</td><td className="num">{pct(m.best_mutation)}</td>
<td className="num nowrap">{dur(m.avg_duration_s)}</td></tr>
))}
</tbody>
</table>
</div>
</div>
<div>
<h3 className="trk-h">{t('trk.allRuns')}</h3>
<div style={{ overflowX: 'auto' }}>
<table className="data-table">
<thead><tr><th>#</th><th>{t('trk.col.date')}</th><th>{t('trk.col.origin')}</th><th>{t('trk.col.model')}</th>
<th>{t('auto.col.status')}</th><th className="num">{t('hist.tests')}</th><th className="num">{t('ev.col.cov')}</th>
<th className="num">{t('ev.col.mut')}</th><th className="num">{t('trk.col.rounds')}</th><th className="num">{t('trk.col.time')}</th>
<th>{t('auto.col.reports')}</th></tr></thead>
<tbody>
{[...d.runs].reverse().map((r, i) => {
const best = d.best?.job_id === r.job_id;
return (
<tr key={r.job_id} className={best ? 'selected' : ''}>
<td className="num">{d.runs.length - i}{best && <span title={t('trk.bestRun')}> ★</span>}</td>
<td className="nowrap"><a href={`#/job/${r.job_id}`}>{date(r.created_at)}</a></td>
<td><span className="tag queued">{t(`trk.origin.${r.origin}` as MsgKey)}</span></td>
<td className="nowrap"><code>{r.model ? modelName(r.model, health) : '—'}</code></td>
<td><span className={`tag ${r.status === 'done' && (!r.total || r.passed !== r.total) ? 'paused' : r.status}`}>
{t(`exp.status.${r.status === 'queued' ? 'pending' : r.status}` as MsgKey)}</span></td>
<td className="num">{r.total != null ? `${r.passed}/${r.total}` : '—'}</td>
<td className="num">{pct(r.coverage)}</td>
<td className="num nowrap">{r.mutation_before != null && r.mutation != null && r.mutation_before !== r.mutation
? `${pct(r.mutation_before, 0)} → ${pct(r.mutation, 0)}` : pct(r.mutation)}</td>
<td className="num">{r.iterations}</td>
<td className="num nowrap">{dur(r.duration_s)}</td>
<td className="nowrap">
<a className="rep-link html" target="_blank" rel="noreferrer" href={`/api/jobs/${r.job_id}/report.html?lang=${lang}`}>HTML</a>{' '}
<a className="rep-link pdf" target="_blank" rel="noreferrer" href={`/api/jobs/${r.job_id}/report.html?lang=${lang}&print=1`}>PDF</a>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
</div>
</>)}
</div>
</div>
)}
</div>
);
}