fix: improve the ui for the status page

This commit is contained in:
2026-08-29 19:23:35 +07:00
parent 621afe1124
commit 1cb94f7f05
4 changed files with 79 additions and 54 deletions
+19 -19
View File
@@ -1,14 +1,17 @@
import type { PublicService } from '../api/status';
import { useMediaQuery } from '../lib/useMediaQuery';
const DAY_MS = 24 * 60 * 60 * 1000;
// Desktop mirrors the API's full 90-day history. Compact screens use the most
// recent 45 days so each daily bar remains legible.
const HISTORY_DAYS = 90;
const DAYS_PER_SEGMENT = 2;
const HISTORY_SEGMENTS = HISTORY_DAYS / DAYS_PER_SEGMENT;
const HISTORY_DAYS_COMPACT = 45;
const COMPACT_QUERY = '(max-width: 520px)';
type HistoryEntry = PublicService['history'][number];
function dayClass(uptimePct: number | null) {
if (uptimePct === null) return 'is-empty';
function dayClass(uptimePct: number | null | undefined) {
if (uptimePct === null || uptimePct === undefined) return 'is-empty';
if (uptimePct === 100) return 'is-up';
if (uptimePct === 0) return 'is-down';
return 'is-partial';
@@ -23,34 +26,31 @@ function formatDay(day: number) {
}).format(new Date(day));
}
function segmentTitle(startDay: number, endDay: number, uptimePct: number | null) {
const dateRange = `${formatDay(startDay)} ${formatDay(endDay)}`;
return `${dateRange}: ${uptimePct === null ? 'No data' : `${uptimePct.toFixed(1)}% uptime`}`;
function dayTitle(day: number, uptimePct: number | null | undefined) {
const detail = uptimePct === null || uptimePct === undefined ? 'No data' : `${uptimePct.toFixed(1)}% uptime`;
return `${formatDay(day)}: ${detail}`;
}
export function StatusHistoryBar({ history }: { history: HistoryEntry[] }) {
const compact = useMediaQuery(COMPACT_QUERY);
const windowDays = compact ? HISTORY_DAYS_COMPACT : HISTORY_DAYS;
const historyByDay = new Map(history.map((entry) => [entry.day, entry.uptimePct]));
const now = new Date();
const today = Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate());
const segments = Array.from({ length: HISTORY_SEGMENTS }, (_segment, index) => {
const startDay = today - (HISTORY_DAYS - index * DAYS_PER_SEGMENT - 1) * DAY_MS;
const endDay = startDay + (DAYS_PER_SEGMENT - 1) * DAY_MS;
const values = Array.from({ length: DAYS_PER_SEGMENT }, (_day, dayIndex) => historyByDay.get(startDay + dayIndex * DAY_MS)).filter(
(value): value is number => value !== undefined && value !== null,
);
const uptimePct = values.length === 0 ? null : Math.round((values.reduce((sum, value) => sum + value, 0) / values.length) * 10) / 10;
return { startDay, endDay, uptimePct };
const days = Array.from({ length: windowDays }, (_day, index) => {
const day = today - (windowDays - 1 - index) * DAY_MS;
return { day, uptimePct: historyByDay.get(day) };
});
return (
<div className="status-history">
<div className="uptime-days" aria-label="90-day uptime shown in 45 two-day segments">
{segments.map(({ startDay, endDay, uptimePct }) => (
<span className={`uptime-day ${dayClass(uptimePct)}`} key={startDay} title={segmentTitle(startDay, endDay, uptimePct)} />
<div className="uptime-days" aria-label={`Daily uptime over the last ${windowDays} days`}>
{days.map(({ day, uptimePct }) => (
<span className={`uptime-day ${dayClass(uptimePct)}`} key={day} title={dayTitle(day, uptimePct)} />
))}
</div>
<div className="uptime-days-caption" aria-hidden="true">
<span>90 days ago</span>
<span>{windowDays} days ago</span>
<i />
<span>Today</span>
</div>
+15
View File
@@ -0,0 +1,15 @@
import { useCallback, useSyncExternalStore } from 'react';
export function useMediaQuery(query: string) {
const subscribe = useCallback(
(listener: () => void) => {
const mediaQuery = window.matchMedia(query);
mediaQuery.addEventListener('change', listener);
return () => mediaQuery.removeEventListener('change', listener);
},
[query],
);
const getSnapshot = useCallback(() => window.matchMedia(query).matches, [query]);
return useSyncExternalStore(subscribe, getSnapshot, () => false);
}
+14 -14
View File
@@ -101,7 +101,7 @@ export function StatusPage() {
</div>
</div>
) : statusQuery.isError || !status ? (
<Empty variant="error" className="mt-[42px] min-h-[280px] place-content-center p-12">
<Empty variant="error" className="mt-10.5 min-h-70 place-content-center p-12">
<EmptyMedia variant="icon">
<TriangleAlert />
</EmptyMedia>
@@ -197,20 +197,20 @@ export function StatusPage() {
const serviceStatus = SERVICE_STATUS[service.status];
return (
<article className="public-service-row" key={service.id}>
<div className="public-service-summary">
<span className="service-icon">
<SiteIcon monitorId={service.id} favicon="public" />
</span>
<div>
<strong>{service.name}</strong>
<Badge className="mt-1.75" variant={serviceStatus.className}>
{serviceStatus.label}
</Badge>
<div className="public-service-header">
<div className="public-service-summary">
<span className="service-icon">
<SiteIcon monitorId={service.id} favicon="public" />
</span>
<div className="public-service-identity">
<strong>{service.name}</strong>
<Badge variant={serviceStatus.className}>{serviceStatus.label}</Badge>
</div>
</div>
<div className="public-service-uptime">
<span>90-day uptime</span>
<strong>{service.uptime90d === null ? '—' : `${service.uptime90d.toFixed(1)}%`}</strong>
</div>
</div>
<div className="public-service-uptime">
<span>90-day uptime</span>
<strong>{service.uptime90d === null ? '—' : `${service.uptime90d.toFixed(1)}%`}</strong>
</div>
<StatusHistoryBar history={service.history} />
</article>
+31 -21
View File
@@ -1712,11 +1712,9 @@ button {
color: var(--muted);
}
.public-service-row {
display: grid;
grid-template-columns: minmax(190px, 0.7fr) 120px minmax(320px, 1.2fr);
align-items: center;
gap: 24px;
min-height: 128px;
display: flex;
flex-direction: column;
gap: 16px;
padding: 22px;
}
.public-service-row + .public-service-row {
@@ -1725,28 +1723,41 @@ button {
.public-service-row:hover {
background: #fdfefd;
}
.public-service-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.public-service-summary {
display: flex;
flex: 1 1 auto;
align-items: center;
gap: 14px;
min-width: 0;
}
.public-service-summary > div {
.public-service-identity {
display: flex;
flex: 1 1 auto;
align-items: center;
gap: 20px;
min-width: 0;
}
.public-service-summary strong {
display: block;
.public-service-identity strong {
min-width: 0;
overflow: hidden;
font-size: 15px;
font-weight: 500;
text-overflow: ellipsis;
white-space: nowrap;
}
.public-service-summary .row-status {
margin-top: 7px;
.public-service-identity [data-slot='badge'] {
flex: 0 0 auto;
}
.public-service-uptime {
flex: 0 0 auto;
text-align: right;
white-space: nowrap;
}
.public-service-uptime span {
display: block;
@@ -1762,6 +1773,7 @@ button {
letter-spacing: -0.5px;
}
.status-history {
width: 100%;
min-width: 0;
}
.uptime-days {
@@ -1773,7 +1785,7 @@ button {
}
.uptime-day {
flex: 1 1 0;
min-width: 1px;
min-width: 2px;
border-radius: 2px;
background: #e6e8e7;
transition:
@@ -1830,11 +1842,10 @@ button {
background: #fff;
}
.status-service-skeleton > div {
display: grid;
grid-template-columns: 42px 1fr minmax(240px, 0.8fr);
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 16px;
min-height: 116px;
padding: 22px;
}
.status-service-skeleton > div + div {
@@ -1858,6 +1869,7 @@ button {
height: 34px;
}
.status-service-skeleton b {
width: 100%;
height: 32px;
}
.status-footer {
@@ -2070,12 +2082,8 @@ button {
grid-column: 2;
justify-self: start;
}
.public-service-row {
grid-template-columns: 1fr auto;
gap: 20px;
}
.status-history {
grid-column: 1 / -1;
.uptime-days {
gap: 1px;
}
}
@@ -2213,12 +2221,14 @@ button {
.public-service-row {
padding: 20px 16px;
}
.public-service-header {
gap: 12px;
}
.public-service-uptime strong {
font-size: 15px;
}
.uptime-days {
height: 28px;
gap: 1px;
}
.status-footer {
align-items: flex-start;