feat(frontend): polish dashboard chart panels
This commit is contained in:
@@ -31,7 +31,9 @@ function BarMetric({ label, value, max, tone = "neutral" }: { label: string; val
|
||||
);
|
||||
}
|
||||
|
||||
function MiniHistogram({ data }: { data: Array<{ label: string; value: number; tone: string }> }) {
|
||||
type ChartPoint = { label: string; value: number; tone: string };
|
||||
|
||||
function MiniHistogram({ data }: { data: ChartPoint[] }) {
|
||||
const max = Math.max(...data.map((item) => item.value), 1);
|
||||
return (
|
||||
<div className="histogram" aria-label="histogram">
|
||||
@@ -51,6 +53,18 @@ function MiniHistogram({ data }: { data: Array<{ label: string; value: number; t
|
||||
);
|
||||
}
|
||||
|
||||
function StatusStack({ data }: { data: ChartPoint[] }) {
|
||||
const total = data.reduce((sum, item) => sum + item.value, 0);
|
||||
return (
|
||||
<div className="status-stack" aria-label="status distribution">
|
||||
{data.map((item) => {
|
||||
const width = total > 0 ? Math.max(6, Math.round((item.value / total) * 100)) : 25;
|
||||
return <i className={item.tone} key={item.label} style={{ width: `${width}%` }} title={`${item.label}: ${item.value}`} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DashboardPage() {
|
||||
const [jobs, setJobs] = useState<Job[]>([]);
|
||||
const [events, setEvents] = useState<JobEvent[]>([]);
|
||||
@@ -110,6 +124,11 @@ export function DashboardPage() {
|
||||
.map(([type, value]) => ({ label: eventTitle(type), value, tone: type.includes("fail") ? "failed" : type.includes("success") ? "succeeded" : "running" }));
|
||||
}, [events]);
|
||||
const maxPressure = Math.max(...pressureData.map((item) => item.value), 1);
|
||||
const activePressure = stats.by_status.queued + stats.by_status.running + stats.retries_pending + stats.overdue_running;
|
||||
const completionRate = recentJobs.length
|
||||
? Math.round((recentJobs.filter((job) => job.status === "succeeded").length / recentJobs.length) * 100)
|
||||
: 0;
|
||||
const topEvent = eventData.at(0);
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
@@ -131,7 +150,7 @@ export function DashboardPage() {
|
||||
</section>
|
||||
|
||||
<section className="dashboard-grid">
|
||||
<section className="panel">
|
||||
<section className="panel chart-panel">
|
||||
<div className="section-title">
|
||||
<h3>Recent Jobs</h3>
|
||||
<Link className="secondary-button" to="/jobs">
|
||||
@@ -140,6 +159,11 @@ export function DashboardPage() {
|
||||
</div>
|
||||
{recentJobs.length ? (
|
||||
<>
|
||||
<div className="chart-summary-row">
|
||||
<span>{recentJobs.length} latest jobs</span>
|
||||
<strong>{completionRate}% succeeded</strong>
|
||||
</div>
|
||||
<StatusStack data={recentJobStatusData} />
|
||||
<MiniHistogram data={recentJobStatusData} />
|
||||
<div className="job-tick-chart" aria-label="latest jobs">
|
||||
{recentJobs.slice(0, 16).map((job) => (
|
||||
@@ -152,11 +176,15 @@ export function DashboardPage() {
|
||||
)}
|
||||
</section>
|
||||
|
||||
<section className="panel">
|
||||
<section className="panel chart-panel">
|
||||
<div className="section-title">
|
||||
<h3>Queue Pressure</h3>
|
||||
<Clock3 size={16} />
|
||||
</div>
|
||||
<div className="chart-summary-row">
|
||||
<span>Active pressure</span>
|
||||
<strong>{activePressure}</strong>
|
||||
</div>
|
||||
<div className="metric-bars">
|
||||
{pressureData.map((item) => (
|
||||
<BarMetric key={item.label} label={item.label} max={maxPressure} tone={item.tone} value={item.value} />
|
||||
@@ -167,12 +195,22 @@ export function DashboardPage() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="panel">
|
||||
<section className="panel chart-panel">
|
||||
<div className="section-title">
|
||||
<h3>Recent Events</h3>
|
||||
<RotateCcw size={16} />
|
||||
</div>
|
||||
{eventData.length ? <MiniHistogram data={eventData} /> : <EmptyState title="No events yet" />}
|
||||
{eventData.length ? (
|
||||
<>
|
||||
<div className="chart-summary-row">
|
||||
<span>{events.length} latest events</span>
|
||||
<strong>{topEvent ? topEvent.label : "No events"}</strong>
|
||||
</div>
|
||||
<MiniHistogram data={eventData} />
|
||||
</>
|
||||
) : (
|
||||
<EmptyState title="No events yet" />
|
||||
)}
|
||||
</section>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -442,6 +442,49 @@ label {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.chart-panel {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chart-panel::before {
|
||||
background: linear-gradient(90deg, var(--cyan), var(--green), var(--orange), var(--red));
|
||||
content: "";
|
||||
height: 3px;
|
||||
left: 14px;
|
||||
opacity: 0.78;
|
||||
position: absolute;
|
||||
right: 14px;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.chart-summary-row {
|
||||
align-items: center;
|
||||
background: var(--panel-raised);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
min-height: 42px;
|
||||
padding: 9px 10px;
|
||||
}
|
||||
|
||||
.chart-summary-row span {
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.chart-summary-row strong {
|
||||
font-size: 14px;
|
||||
overflow: hidden;
|
||||
text-align: right;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.detail-grid {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
@@ -645,9 +688,21 @@ label {
|
||||
display: flex;
|
||||
height: 112px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.histogram-column::before {
|
||||
background:
|
||||
linear-gradient(to top, transparent 24%, rgb(127 139 147 / 18%) 25%, transparent 26%),
|
||||
linear-gradient(to top, transparent 49%, rgb(127 139 147 / 18%) 50%, transparent 51%),
|
||||
linear-gradient(to top, transparent 74%, rgb(127 139 147 / 18%) 75%, transparent 76%);
|
||||
content: "";
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.histogram-column i,
|
||||
.metric-track i {
|
||||
background: var(--accent);
|
||||
@@ -656,7 +711,9 @@ label {
|
||||
|
||||
.histogram-column i {
|
||||
border-radius: 6px 6px 0 0;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.histogram-column i.queued,
|
||||
@@ -703,6 +760,38 @@ label {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.status-stack {
|
||||
background: var(--panel-raised);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 999px;
|
||||
display: flex;
|
||||
gap: 3px;
|
||||
height: 14px;
|
||||
margin-bottom: 12px;
|
||||
overflow: hidden;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.status-stack i {
|
||||
background: var(--subtle);
|
||||
border-radius: 999px;
|
||||
display: block;
|
||||
min-width: 6px;
|
||||
}
|
||||
|
||||
.status-stack i.queued,
|
||||
.status-stack i.running {
|
||||
background: var(--cyan);
|
||||
}
|
||||
|
||||
.status-stack i.succeeded {
|
||||
background: var(--green);
|
||||
}
|
||||
|
||||
.status-stack i.failed {
|
||||
background: var(--red);
|
||||
}
|
||||
|
||||
.job-tick {
|
||||
background: var(--subtle);
|
||||
border-radius: 5px;
|
||||
|
||||
Reference in New Issue
Block a user