mirror of
https://github.com/Nezumi-2711/uptime-monitoring.git
synced 2026-09-22 13:48:31 +00:00
fix: improve the moditor detail page
This commit is contained in:
@@ -26,7 +26,7 @@ export function LatencySparkline({ checks }: { checks: Check[] }) {
|
|||||||
if (data.length < 2)
|
if (data.length < 2)
|
||||||
return (
|
return (
|
||||||
<Empty className="min-h-37.5 p-6">
|
<Empty className="min-h-37.5 p-6">
|
||||||
<EmptyTitle className="text-[13px]">Not enough latency data</EmptyTitle>
|
<EmptyTitle className="text-caption">Not enough latency data</EmptyTitle>
|
||||||
<EmptyDescription>More checks are needed to draw latency.</EmptyDescription>
|
<EmptyDescription>More checks are needed to draw latency.</EmptyDescription>
|
||||||
</Empty>
|
</Empty>
|
||||||
);
|
);
|
||||||
@@ -35,7 +35,11 @@ export function LatencySparkline({ checks }: { checks: Check[] }) {
|
|||||||
const maximum = Math.max(...values);
|
const maximum = Math.max(...values);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="sparkline-wrap" role="img" aria-label={`Latency from ${minimum} to ${maximum} milliseconds`}>
|
<div
|
||||||
|
className="relative h-61.25 p-[26px_24px_24px] bg-linear-to-b from-surface-sparkline to-white dark:bg-[linear-gradient(180deg,rgb(62_207_142/0.025),transparent_55%),var(--color-night-chart)] dark:[&_.recharts-cartesian-grid_line]:stroke-white/7.5"
|
||||||
|
role="img"
|
||||||
|
aria-label={`Latency from ${minimum} to ${maximum} milliseconds`}
|
||||||
|
>
|
||||||
<ChartContainer config={latencyConfig} className="h-full w-full">
|
<ChartContainer config={latencyConfig} className="h-full w-full">
|
||||||
<AreaChart data={data} margin={{ top: 10, right: 4, bottom: 4, left: 4 }}>
|
<AreaChart data={data} margin={{ top: 10, right: 4, bottom: 4, left: 4 }}>
|
||||||
<defs>
|
<defs>
|
||||||
@@ -50,7 +54,7 @@ export function LatencySparkline({ checks }: { checks: Check[] }) {
|
|||||||
<ChartTooltip
|
<ChartTooltip
|
||||||
content={
|
content={
|
||||||
<ChartTooltipContent
|
<ChartTooltipContent
|
||||||
className="monitor-chart-tooltip min-w-36 gap-1.5 rounded-[6px] border-(--hairline) bg-white/97 px-3 py-2.5 shadow-[0_8px_24px_rgb(24_74_52/0.1),0_2px_6px_rgb(0_0_0/0.04)] backdrop-blur-sm"
|
className="min-w-36 gap-1.5 rounded-sm border-hairline bg-white/97 px-3 py-2.5 shadow-[0_8px_24px_rgb(24_74_52/0.1),0_2px_6px_rgb(0_0_0/0.04)] backdrop-blur-sm dark:border-white/12 dark:bg-night-surface/96 dark:text-night-body dark:shadow-[0_16px_40px_rgb(0_0_0/0.55),inset_0_1px_rgb(255_255_255/0.045)]"
|
||||||
labelFormatter={(_, payload) => new Date((payload[0].payload as LatencyDatum).t).toLocaleString()}
|
labelFormatter={(_, payload) => new Date((payload[0].payload as LatencyDatum).t).toLocaleString()}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -71,7 +75,7 @@ export function LatencySparkline({ checks }: { checks: Check[] }) {
|
|||||||
/>
|
/>
|
||||||
</AreaChart>
|
</AreaChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
<div className="chart-scale">
|
<div className="pointer-events-none absolute inset-[19px_22px_20px_auto] flex flex-col justify-between font-mono text-[9px]/none font-normal text-ink-faint-alt dark:text-night-scale">
|
||||||
<span>{maximum} ms</span>
|
<span>{maximum} ms</span>
|
||||||
<span>{minimum} ms</span>
|
<span>{minimum} ms</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ const formatUptimeTooltip: NonNullable<ComponentProps<typeof ChartTooltipContent
|
|||||||
return (
|
return (
|
||||||
<div className="flex w-full items-center justify-between gap-5">
|
<div className="flex w-full items-center justify-between gap-5">
|
||||||
<span className="text-(--muted)">Successful</span>
|
<span className="text-(--muted)">Successful</span>
|
||||||
<span className="font-mono font-medium tabular-nums text-(--ink)">
|
<span className="font-mono font-medium tabular-nums text-ink">
|
||||||
{point.successfulChecks}/{point.totalChecks}
|
{point.successfulChecks}/{point.totalChecks}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -83,7 +83,7 @@ export function UptimeBar({ checks }: { checks: Check[] }) {
|
|||||||
if (data.length === 0)
|
if (data.length === 0)
|
||||||
return (
|
return (
|
||||||
<Empty className="min-h-37.5 p-6">
|
<Empty className="min-h-37.5 p-6">
|
||||||
<EmptyTitle className="text-[13px]">No availability data yet</EmptyTitle>
|
<EmptyTitle className="text-caption">No availability data yet</EmptyTitle>
|
||||||
<EmptyDescription>No availability checks recorded yet.</EmptyDescription>
|
<EmptyDescription>No availability checks recorded yet.</EmptyDescription>
|
||||||
</Empty>
|
</Empty>
|
||||||
);
|
);
|
||||||
@@ -91,7 +91,11 @@ export function UptimeBar({ checks }: { checks: Check[] }) {
|
|||||||
const successfulChecks = checks.filter((check) => check.ok).length;
|
const successfulChecks = checks.filter((check) => check.ok).length;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="uptime-bar" role="img" aria-label={`${successfulChecks} of ${checks.length} recent checks succeeded`}>
|
<div
|
||||||
|
className="h-dialog-btn p-[24px_22px_18px] bg-linear-to-b from-surface-uptime to-white dark:bg-[linear-gradient(180deg,rgb(62_207_142/0.025),transparent_55%),var(--color-night-chart)]"
|
||||||
|
role="img"
|
||||||
|
aria-label={`${successfulChecks} of ${checks.length} recent checks succeeded`}
|
||||||
|
>
|
||||||
<ChartContainer config={uptimeConfig} className="h-full w-full">
|
<ChartContainer config={uptimeConfig} className="h-full w-full">
|
||||||
<BarChart data={data} barCategoryGap="24%" margin={{ top: 6, right: 0, bottom: 0, left: 0 }}>
|
<BarChart data={data} barCategoryGap="24%" margin={{ top: 6, right: 0, bottom: 0, left: 0 }}>
|
||||||
<XAxis dataKey="id" hide />
|
<XAxis dataKey="id" hide />
|
||||||
@@ -99,7 +103,7 @@ export function UptimeBar({ checks }: { checks: Check[] }) {
|
|||||||
<ChartTooltip
|
<ChartTooltip
|
||||||
content={
|
content={
|
||||||
<ChartTooltipContent
|
<ChartTooltipContent
|
||||||
className="monitor-chart-tooltip min-w-36 gap-1.5 rounded-[6px] border-(--hairline) bg-white/97 px-3 py-2.5 shadow-[0_8px_24px_rgb(24_74_52/0.1),0_2px_6px_rgb(0_0_0/0.04)] backdrop-blur-sm"
|
className="min-w-36 gap-1.5 rounded-sm border-hairline bg-white/97 px-3 py-2.5 shadow-[0_8px_24px_rgb(24_74_52/0.1),0_2px_6px_rgb(0_0_0/0.04)] backdrop-blur-sm dark:border-white/12 dark:bg-night-surface/96 dark:text-night-body dark:shadow-[0_16px_40px_rgb(0_0_0/0.55),inset_0_1px_rgb(255_255_255/0.045)]"
|
||||||
hideIndicator={false}
|
hideIndicator={false}
|
||||||
nameKey="status"
|
nameKey="status"
|
||||||
labelFormatter={(_, payload) => formatBucketTime(payload[0].payload as UptimeDatum)}
|
labelFormatter={(_, payload) => formatBucketTime(payload[0].payload as UptimeDatum)}
|
||||||
@@ -123,12 +127,13 @@ export function UptimeBar({ checks }: { checks: Check[] }) {
|
|||||||
</BarChart>
|
</BarChart>
|
||||||
</ChartContainer>
|
</ChartContainer>
|
||||||
</div>
|
</div>
|
||||||
<div className="uptime-legend">
|
<div className="flex justify-between border-t border-border-divider p-[14px_22px_0] text-2xs text-ink-subtle-2 dark:border-white/6.5 dark:bg-night-elevated dark:text-night-faint-alt">
|
||||||
<span>Oldest</span>
|
<span className="inline-flex items-center gap-1.25">Oldest</span>
|
||||||
<span>
|
<span className="inline-flex items-center gap-1.25">
|
||||||
<i className="legend-up" /> Up <i className="legend-degraded" /> Degraded <i className="legend-down" /> Down
|
<i className="size-1.5 rounded-full bg-primary-deep" /> Up <i className="ml-1.25 size-1.5 rounded-full bg-chart-degraded" />{' '}
|
||||||
|
Degraded <i className="ml-1.25 size-1.5 rounded-full bg-chart-down" /> Down
|
||||||
</span>
|
</span>
|
||||||
<span>Latest</span>
|
<span className="inline-flex items-center gap-1.25">Latest</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Card } from '@/components/ui/card';
|
import { Card } from '@/components/ui/card';
|
||||||
import { Empty, EmptyContent, EmptyDescription, EmptyTitle } from '@/components/ui/empty';
|
import { Empty, EmptyContent, EmptyDescription, EmptyTitle } from '@/components/ui/empty';
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
import { AppHeader } from '../components/AppHeader';
|
import { AppHeader } from '../components/AppHeader';
|
||||||
import { LatencySparkline } from '../components/charts/LatencySparkline';
|
import { LatencySparkline } from '../components/charts/LatencySparkline';
|
||||||
import { UptimeBar } from '../components/charts/UptimeBar';
|
import { UptimeBar } from '../components/charts/UptimeBar';
|
||||||
@@ -23,6 +24,18 @@ import {
|
|||||||
useRunCheckMutation,
|
useRunCheckMutation,
|
||||||
useUpdateMonitorMutation,
|
useUpdateMonitorMutation,
|
||||||
} from '../queries/monitors';
|
} from '../queries/monitors';
|
||||||
|
|
||||||
|
const statusOrbVariants: Record<string, string> = {
|
||||||
|
online:
|
||||||
|
'border-status-online-border bg-primary-deep shadow-[0_0_0_8px_white] dark:border-brand/22 dark:bg-brand dark:shadow-[0_0_0_8px_var(--color-night-base),0_0_24px_rgb(62_207_142/0.38)]',
|
||||||
|
offline:
|
||||||
|
'border-status-offline-border bg-status-offline-bg shadow-[0_0_0_8px_white] dark:border-red-400/24 dark:bg-red-400 dark:shadow-[0_0_0_8px_var(--color-night-base),0_0_22px_rgb(248_113_113/0.32)]',
|
||||||
|
pending:
|
||||||
|
'border-status-pending-border bg-status-pending-bg shadow-[0_0_0_8px_white] dark:border-amber-500/24 dark:bg-status-pending-dark dark:shadow-[0_0_0_8px_var(--color-night-base),0_0_22px_rgb(245_158_11/0.28)]',
|
||||||
|
checking:
|
||||||
|
'border-status-checking-border bg-status-checking-bg shadow-[0_0_0_8px_white] dark:border-yellow-400/22 dark:bg-status-checking-dark dark:shadow-[0_0_0_8px_var(--color-night-base),0_0_22px_rgb(250_204_21/0.25)]',
|
||||||
|
};
|
||||||
|
|
||||||
function formatInterval(seconds: number) {
|
function formatInterval(seconds: number) {
|
||||||
return INTERVAL_OPTIONS.find((option) => Number(option.value) === seconds)?.label ?? `${Math.round(seconds / 60)} min`;
|
return INTERVAL_OPTIONS.find((option) => Number(option.value) === seconds)?.label ?? `${Math.round(seconds / 60)} min`;
|
||||||
}
|
}
|
||||||
@@ -76,29 +89,36 @@ export function MonitorDetailPage({ id }: { id: number }) {
|
|||||||
onCancel={() => setIsConfirmingDelete(false)}
|
onCancel={() => setIsConfirmingDelete(false)}
|
||||||
onConfirm={() => deleteMutation.mutate(monitor.id, { onSuccess: () => navigate('/dashboard') })}
|
onConfirm={() => deleteMutation.mutate(monitor.id, { onSuccess: () => navigate('/dashboard') })}
|
||||||
/>
|
/>
|
||||||
<main className="dashboard-main detail-main">
|
<main className="dashboard-main pt-8.5 dark:relative dark:isolate dark:before:pointer-events-none dark:before:absolute dark:before:-top-27.5 dark:before:left-1/2 dark:before:-z-1 dark:before:h-107.5 dark:before:w-[min(920px,92vw)] dark:before:-translate-x-1/2 dark:before:content-[''] dark:before:[background:radial-gradient(circle_at_35%_20%,rgb(62_207_142/0.075),transparent_45%),linear-gradient(90deg,rgb(255_255_255/0.018)_1px,transparent_1px),linear-gradient(rgb(255_255_255/0.018)_1px,transparent_1px)] dark:before:bg-size-[auto,44px_44px,44px_44px] dark:before:mask-[linear-gradient(to_bottom,black_0%,transparent_82%)] max-[520px]:dark:before:-top-20 max-[520px]:dark:before:h-82.5 max-[520px]:dark:before:bg-size-[auto,34px_34px,34px_34px]">
|
||||||
<Button variant="unstyled" className="back-link" type="button" onClick={() => navigate('/dashboard')}>
|
<Button variant="unstyled" className="back-link" type="button" onClick={() => navigate('/dashboard')}>
|
||||||
<ArrowLeft /> All monitors
|
<ArrowLeft /> All monitors
|
||||||
</Button>
|
</Button>
|
||||||
<section className="detail-hero">
|
<section className="mt-8.5 flex flex-col items-start justify-between gap-8 border-b border-border-hero pb-9 min-[761px]:flex-row min-[761px]:items-end dark:border-white/9 dark:animate-monitor-enter">
|
||||||
<div className="detail-title">
|
<div className="flex min-w-0 items-center gap-4.5 max-[520px]:items-start">
|
||||||
<span className={`status-orb ${status.variant}`} />
|
<span className={cn('size-4 shrink-0 rounded-full border-4 max-[520px]:mt-2.75', statusOrbVariants[status.variant])} />
|
||||||
<div>
|
<div>
|
||||||
<p className="overline">Monitor #{monitor.id}</p>
|
<p className="overline">Monitor #{monitor.id}</p>
|
||||||
<h1>{monitor.name}</h1>
|
<h1 className="m-0 truncate text-display-sm font-medium leading-[1.1] tracking-display text-foreground min-[761px]:text-display dark:text-slate-50 dark:[text-shadow:0_1px_18px_rgb(0_0_0/0.22)]">
|
||||||
<a href={monitor.url} target="_blank" rel="noreferrer">
|
{monitor.name}
|
||||||
|
</h1>
|
||||||
|
<a
|
||||||
|
href={monitor.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="mt-2.25 inline-flex max-w-[min(70vw,680px)] items-center gap-1.5 truncate font-mono text-caption/overline font-normal text-(--muted) hover:text-primary-deep dark:text-night-muted-text dark:hover:text-brand-soft"
|
||||||
|
>
|
||||||
{monitor.url}
|
{monitor.url}
|
||||||
<ExternalLink />
|
<ExternalLink className="size-3.25 shrink-0" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="detail-actions">
|
<div className="flex items-center gap-4.5 max-[760px]:flex-wrap">
|
||||||
<Badge variant={status.variant} title={status.detail ?? undefined}>
|
<Badge variant={status.variant} title={status.detail ?? undefined}>
|
||||||
{status.label}
|
{status.label}
|
||||||
</Badge>
|
</Badge>
|
||||||
{!monitor.alertsEnabled && (
|
{!monitor.alertsEnabled && (
|
||||||
<span className="muted-alert">
|
<span className="inline-flex items-center gap-1.5 text-micro text-(--muted) dark:text-night-muted-text">
|
||||||
<BellOff /> Alerts muted
|
<BellOff className="size-3.5" /> Alerts muted
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
@@ -113,15 +133,20 @@ export function MonitorDetailPage({ id }: { id: number }) {
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section className="sla-grid" aria-label="Uptime windows">
|
<section
|
||||||
|
className="mt-6 grid grid-cols-1 gap-3 min-[521px]:grid-cols-2 min-[761px]:grid-cols-3 min-[1001px]:grid-cols-5 dark:animate-monitor-enter dark:[animation-delay:45ms]"
|
||||||
|
aria-label="Uptime windows"
|
||||||
|
>
|
||||||
{(['24h', '7d', '30d', '90d'] as const).map((key) => {
|
{(['24h', '7d', '30d', '90d'] as const).map((key) => {
|
||||||
const window = statsQuery.data?.windows[key];
|
const window = statsQuery.data?.windows[key];
|
||||||
return (
|
return (
|
||||||
<Card asChild key={key}>
|
<Card asChild key={key}>
|
||||||
<article className="sla-card">
|
<article className="min-w-0 rounded-md border border-border-card bg-white p-5 transition-[border-color,transform,box-shadow] duration-180 max-[520px]:p-4.5 dark:relative dark:border-white/8.5 dark:bg-[linear-gradient(145deg,var(--color-night-surface)_0%,var(--color-night-elevated)_100%)] dark:shadow-[0_10px_26px_rgb(0_0_0/0.22),inset_0_1px_rgb(255_255_255/0.035)] dark:before:absolute dark:before:-top-px dark:before:left-5 dark:before:h-px dark:before:w-8.5 dark:before:bg-brand/65 dark:before:content-[''] dark:hover:border-brand/24 dark:hover:-translate-y-0.5 dark:hover:shadow-[0_14px_34px_rgb(0_0_0/0.3),0_0_0_1px_rgb(62_207_142/0.035),inset_0_1px_rgb(255_255_255/0.05)]">
|
||||||
<p>{key} uptime</p>
|
<p className="m-0 mb-4.25 text-micro text-(--muted) dark:text-night-muted-text">{key} uptime</p>
|
||||||
<strong>{window?.uptimePct == null ? '—' : `${window.uptimePct.toFixed(3)}%`}</strong>
|
<strong className="block font-mono text-[23px]/none font-medium tracking-metric text-foreground dark:text-night-metric">
|
||||||
<span>
|
{window?.uptimePct == null ? '—' : `${window.uptimePct.toFixed(3)}%`}
|
||||||
|
</strong>
|
||||||
|
<span className="mt-2.75 block truncate text-footnote text-faint dark:text-night-faint">
|
||||||
{window?.totalChecks ?? 0} checks · {window?.avgLatencyMs ?? '—'} ms avg
|
{window?.totalChecks ?? 0} checks · {window?.avgLatencyMs ?? '—'} ms avg
|
||||||
</span>
|
</span>
|
||||||
</article>
|
</article>
|
||||||
@@ -129,32 +154,51 @@ export function MonitorDetailPage({ id }: { id: number }) {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<Card asChild>
|
<Card asChild>
|
||||||
<article className={`sla-card incident-summary ${openIncident ? 'has-incident' : ''}`}>
|
<article
|
||||||
<p>Current incident</p>
|
className={cn(
|
||||||
<strong>{openIncident ? formatDuration(null, openIncident.startedAt) : 'None'}</strong>
|
'min-w-0 rounded-md border border-border-card p-5 transition-[border-color,transform,box-shadow] duration-180 max-[520px]:p-4.5 max-[760px]:min-[521px]:col-span-full',
|
||||||
<span>{openIncident ? `Open since ${formatDate(openIncident.startedAt)}` : 'Everything is operational'}</span>
|
"dark:relative dark:border-white/8.5 dark:shadow-[0_10px_26px_rgb(0_0_0/0.22),inset_0_1px_rgb(255_255_255/0.035)] dark:before:absolute dark:before:-top-px dark:before:left-5 dark:before:h-px dark:before:w-8.5 dark:before:content-[''] dark:hover:border-brand/24 dark:hover:-translate-y-0.5 dark:hover:shadow-[0_14px_34px_rgb(0_0_0/0.3),0_0_0_1px_rgb(62_207_142/0.035),inset_0_1px_rgb(255_255_255/0.05)]",
|
||||||
|
openIncident
|
||||||
|
? 'border-incident-border bg-incident-bg dark:border-red-400/30 dark:bg-[linear-gradient(145deg,rgb(73_28_31/0.58),var(--color-night-incident)_100%)] dark:before:bg-red-400'
|
||||||
|
: 'bg-surface-soft dark:bg-[linear-gradient(145deg,var(--color-night-card-alt)_0%,var(--color-night-panel)_100%)] dark:before:bg-slate-400/48',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<p className="m-0 mb-4.25 text-micro text-(--muted) dark:text-night-muted-text">Current incident</p>
|
||||||
|
<strong
|
||||||
|
className={cn(
|
||||||
|
'block font-mono text-[23px]/none font-medium tracking-metric text-foreground dark:text-night-metric',
|
||||||
|
openIncident ? 'text-incident-text dark:text-red-300' : '',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{openIncident ? formatDuration(null, openIncident.startedAt) : 'None'}
|
||||||
|
</strong>
|
||||||
|
<span className="mt-2.75 block truncate text-footnote text-faint dark:text-night-faint">
|
||||||
|
{openIncident ? `Open since ${formatDate(openIncident.startedAt)}` : 'Everything is operational'}
|
||||||
|
</span>
|
||||||
</article>
|
</article>
|
||||||
</Card>
|
</Card>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div className="detail-grid">
|
<div className="mt-4 grid grid-cols-1 gap-4 min-[1001px]:grid-cols-[minmax(0,1.55fr)_minmax(300px,0.75fr)] dark:animate-monitor-enter dark:[animation-delay:90ms]">
|
||||||
<section className="data-panel chart-panel">
|
<section className="min-h-82.5 overflow-hidden rounded-md border border-border-subtle bg-white dark:border-white/8.5 dark:bg-night-panel dark:shadow-[0_16px_38px_rgb(0_0_0/0.27),inset_0_1px_rgb(255_255_255/0.03)]">
|
||||||
<div className="data-panel-heading">
|
<div className="flex min-h-20.5 items-center justify-between gap-5 border-b border-border-row p-[18px_22px] max-[520px]:p-4 dark:border-white/7.5 dark:bg-[linear-gradient(180deg,rgb(255_255_255/0.022),transparent)]">
|
||||||
<div>
|
<div>
|
||||||
<p className="overline">Response time</p>
|
<p className="overline mb-1.25">Response time</p>
|
||||||
<h2>Latency</h2>
|
<h2 className="m-0 text-[18px] font-medium text-foreground dark:text-night-heading">Latency</h2>
|
||||||
</div>
|
</div>
|
||||||
<span>Last {checks.length} checks</span>
|
<span className="font-mono text-footnote/overline font-normal text-faint dark:text-night-subtle-text">
|
||||||
|
Last {checks.length} checks
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<LatencySparkline checks={checks} />
|
<LatencySparkline checks={checks} />
|
||||||
</section>
|
</section>
|
||||||
<section className="data-panel uptime-panel">
|
<section className="min-h-82.5 overflow-hidden rounded-md border border-border-subtle bg-white dark:border-white/8.5 dark:bg-night-panel dark:shadow-[0_16px_38px_rgb(0_0_0/0.27),inset_0_1px_rgb(255_255_255/0.03)]">
|
||||||
<div className="data-panel-heading">
|
<div className="flex min-h-20.5 items-center justify-between gap-5 border-b border-border-row p-[18px_22px] max-[520px]:p-4 dark:border-white/7.5 dark:bg-[linear-gradient(180deg,rgb(255_255_255/0.022),transparent)]">
|
||||||
<div>
|
<div>
|
||||||
<p className="overline">Availability</p>
|
<p className="overline mb-1.25">Availability</p>
|
||||||
<h2>Recent uptime</h2>
|
<h2 className="m-0 text-[18px] font-medium text-foreground dark:text-night-heading">Recent uptime</h2>
|
||||||
</div>
|
</div>
|
||||||
<span>
|
<span className="font-mono text-footnote/overline font-normal text-faint dark:text-night-subtle-text">
|
||||||
{checks.filter((check) => check.ok).length}/{checks.length} successful
|
{checks.filter((check) => check.ok).length}/{checks.length} successful
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -162,18 +206,20 @@ export function MonitorDetailPage({ id }: { id: number }) {
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="detail-grid lower-grid">
|
<div className="mt-4 grid grid-cols-1 items-stretch gap-4 min-[1001px]:grid-cols-[minmax(0,1.55fr)_minmax(300px,0.75fr)] dark:animate-monitor-enter dark:[animation-delay:135ms]">
|
||||||
<section className="data-panel recent-checks-panel">
|
<section className="flex h-full min-h-0 flex-col overflow-hidden rounded-md border border-border-subtle bg-white dark:border-white/8.5 dark:bg-night-panel dark:shadow-[0_16px_38px_rgb(0_0_0/0.27),inset_0_1px_rgb(255_255_255/0.03)]">
|
||||||
<div className="data-panel-heading">
|
<div className="flex min-h-20.5 items-center justify-between gap-5 border-b border-border-row p-[18px_22px] max-[520px]:p-4 dark:border-white/7.5 dark:bg-[linear-gradient(180deg,rgb(255_255_255/0.022),transparent)]">
|
||||||
<div>
|
<div>
|
||||||
<p className="overline">Event stream</p>
|
<p className="overline mb-1.25">Event stream</p>
|
||||||
<h2>Recent checks</h2>
|
<h2 className="m-0 text-[18px] font-medium text-foreground dark:text-night-heading">Recent checks</h2>
|
||||||
</div>
|
</div>
|
||||||
<span>{Math.min(checks.length, 20)} shown</span>
|
<span className="font-mono text-footnote/overline font-normal text-faint dark:text-night-subtle-text">
|
||||||
|
{Math.min(checks.length, 20)} shown
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/* Keyboard focus makes this horizontally scrollable region accessible without a pointer. */}
|
{/* Keyboard focus makes this horizontally scrollable region accessible without a pointer. */}
|
||||||
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */}
|
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */}
|
||||||
<section className="data-table-wrap recent-checks-scroll" aria-label="Recent checks" tabIndex={0}>
|
<section className="monitor-scroll overflow-x-auto" aria-label="Recent checks" tabIndex={0}>
|
||||||
<Table className="min-w-150">
|
<Table className="min-w-150">
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
@@ -185,12 +231,12 @@ export function MonitorDetailPage({ id }: { id: number }) {
|
|||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{checks.slice(0, 20).map((check) => (
|
{checks.slice(0, 20).map((check) => (
|
||||||
<TableRow key={check.id}>
|
<TableRow key={check.id} className="transition-colors duration-140 dark:hover:bg-white/[0.022]">
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Badge variant={check.ok ? 'online' : 'offline'}>{check.ok ? 'Up' : 'Down'}</Badge>
|
<Badge variant={check.ok ? 'online' : 'offline'}>{check.ok ? 'Up' : 'Down'}</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<code className="max-w-65 truncate font-mono text-[11px]/[1.4] font-normal">
|
<code className="max-w-select-max truncate font-mono text-footnote/overline font-normal dark:text-night-code">
|
||||||
{check.statusCode ? `HTTP ${check.statusCode}` : (check.error ?? 'Failed')}
|
{check.statusCode ? `HTTP ${check.statusCode}` : (check.error ?? 'Failed')}
|
||||||
</code>
|
</code>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -202,38 +248,62 @@ export function MonitorDetailPage({ id }: { id: number }) {
|
|||||||
</Table>
|
</Table>
|
||||||
{checks.length === 0 && (
|
{checks.length === 0 && (
|
||||||
<Empty className="min-h-37.5 p-6">
|
<Empty className="min-h-37.5 p-6">
|
||||||
<EmptyTitle className="text-[13px]">No checks yet</EmptyTitle>
|
<EmptyTitle className="text-caption">No checks yet</EmptyTitle>
|
||||||
<EmptyDescription>No checks recorded.</EmptyDescription>
|
<EmptyDescription>No checks recorded.</EmptyDescription>
|
||||||
</Empty>
|
</Empty>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
</section>
|
</section>
|
||||||
<section className="data-panel incidents-panel">
|
<section className="flex h-full min-h-0 flex-col overflow-hidden rounded-md border border-border-subtle bg-white dark:border-white/8.5 dark:bg-night-panel dark:shadow-[0_16px_38px_rgb(0_0_0/0.27),inset_0_1px_rgb(255_255_255/0.03)]">
|
||||||
<div className="data-panel-heading">
|
<div className="flex min-h-20.5 items-center justify-between gap-5 border-b border-border-row p-[18px_22px] max-[520px]:p-4 dark:border-white/7.5 dark:bg-[linear-gradient(180deg,rgb(255_255_255/0.022),transparent)]">
|
||||||
<div>
|
<div>
|
||||||
<p className="overline">Downtime</p>
|
<p className="overline mb-1.25">Downtime</p>
|
||||||
<h2>Incidents</h2>
|
<h2 className="m-0 text-[18px] font-medium text-foreground dark:text-night-heading">Incidents</h2>
|
||||||
</div>
|
</div>
|
||||||
<span>{incidents.length} recorded</span>
|
<span className="font-mono text-footnote/overline font-normal text-faint dark:text-night-subtle-text">
|
||||||
|
{incidents.length} recorded
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{/* Keyboard focus makes this scrollable region accessible without a pointer. */}
|
{/* Keyboard focus makes this scrollable region accessible without a pointer. */}
|
||||||
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */}
|
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex */}
|
||||||
<div className="incident-list" aria-label="Incident history" tabIndex={0}>
|
<div className="monitor-scroll min-h-0" aria-label="Incident history" tabIndex={0}>
|
||||||
{incidents.map((incident) => (
|
{incidents.map((incident) => (
|
||||||
<article className={incident.resolvedAt ? 'resolved' : 'open'} key={incident.id}>
|
<article
|
||||||
<span>{incident.resolvedAt ? <CheckCircle2 /> : <Clock3 />}</span>
|
key={incident.id}
|
||||||
<div>
|
className="flex gap-3.25 p-[18px_20px] not-first:border-t not-first:border-border-row transition-colors duration-140 dark:not-first:border-white/6.5 dark:hover:bg-white/[0.022]"
|
||||||
<button type="button" className="incident-detail-link" onClick={() => navigate(`/incidents/${incident.id}`)}>
|
>
|
||||||
<strong>{incident.title ?? (incident.resolvedAt ? 'Resolved incident' : 'Incident in progress')}</strong>
|
<span
|
||||||
|
className={cn(
|
||||||
|
'grid size-7.5 shrink-0 place-items-center rounded-full [&>svg]:size-3.75',
|
||||||
|
incident.resolvedAt
|
||||||
|
? 'bg-incident-resolved-bg text-accent-green-hover dark:bg-brand/10.5 dark:text-brand-soft dark:shadow-[inset_0_0_0_1px_rgb(62_207_142/0.12)]'
|
||||||
|
: 'bg-incident-active-bg text-incident-active-text dark:bg-red-400/10 dark:text-red-400 dark:shadow-[inset_0_0_0_1px_rgb(248_113_113/0.14)]',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{incident.resolvedAt ? <CheckCircle2 /> : <Clock3 />}
|
||||||
|
</span>
|
||||||
|
<div className="min-w-0">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="group inline-flex cursor-pointer items-center gap-2.5 border-0 bg-transparent p-0 text-inherit"
|
||||||
|
onClick={() => navigate(`/incidents/${incident.id}`)}
|
||||||
|
>
|
||||||
|
<strong className="text-caption font-medium text-foreground transition-colors group-hover:text-brand-deep dark:text-night-title dark:group-hover:text-brand-soft">
|
||||||
|
{incident.title ?? (incident.resolvedAt ? 'Resolved incident' : 'Incident in progress')}
|
||||||
|
</strong>
|
||||||
<Badge variant={incident.resolvedAt ? 'online' : 'offline'}>{incident.status}</Badge>
|
<Badge variant={incident.resolvedAt ? 'online' : 'offline'}>{incident.status}</Badge>
|
||||||
</button>
|
</button>
|
||||||
<p>
|
<p className="my-1.5 mb-1.5 mt-1 break-all text-micro leading-alert text-(--muted) dark:text-gray-400">
|
||||||
{incident.latestUpdate?.body ??
|
{incident.latestUpdate?.body ??
|
||||||
incident.startError ??
|
incident.startError ??
|
||||||
(incident.startStatusCode ? `HTTP ${incident.startStatusCode}` : 'Endpoint became unavailable')}
|
(incident.startStatusCode ? `HTTP ${incident.startStatusCode}` : 'Endpoint became unavailable')}
|
||||||
</p>
|
</p>
|
||||||
{incident.latestUpdate && incident.startError && <small className="incident-raw">{incident.startError}</small>}
|
{incident.latestUpdate && incident.startError && (
|
||||||
<small>
|
<small className="-mt-px mb-1.25 block break-all font-mono text-2xs/overline text-ink-faint-alt dark:text-night-faint-alt">
|
||||||
|
{incident.startError}
|
||||||
|
</small>
|
||||||
|
)}
|
||||||
|
<small className="font-mono text-2xs/overline text-ink-faint-alt dark:text-night-faint-alt">
|
||||||
{formatDate(incident.startedAt)} · {formatDuration(incident.durationMs, incident.startedAt)}
|
{formatDate(incident.startedAt)} · {formatDuration(incident.durationMs, incident.startedAt)}
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
@@ -241,7 +311,7 @@ export function MonitorDetailPage({ id }: { id: number }) {
|
|||||||
))}
|
))}
|
||||||
{incidents.length === 0 && (
|
{incidents.length === 0 && (
|
||||||
<Empty className="min-h-37.5 p-6">
|
<Empty className="min-h-37.5 p-6">
|
||||||
<EmptyTitle className="text-[13px]">No incidents yet</EmptyTitle>
|
<EmptyTitle className="text-caption">No incidents yet</EmptyTitle>
|
||||||
<EmptyDescription>No downtime incidents recorded.</EmptyDescription>
|
<EmptyDescription>No downtime incidents recorded.</EmptyDescription>
|
||||||
</Empty>
|
</Empty>
|
||||||
)}
|
)}
|
||||||
@@ -249,56 +319,71 @@ export function MonitorDetailPage({ id }: { id: number }) {
|
|||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section className="data-panel configuration-panel">
|
<section className="mt-4 overflow-hidden rounded-md border border-border-subtle bg-white dark:border-white/8.5 dark:bg-night-panel dark:shadow-[0_16px_38px_rgb(0_0_0/0.27),inset_0_1px_rgb(255_255_255/0.03)] dark:animate-monitor-enter dark:[animation-delay:180ms]">
|
||||||
<div className="data-panel-heading">
|
<div className="flex min-h-20.5 items-center justify-between gap-5 border-b border-border-row p-[18px_22px] max-[520px]:p-4 dark:border-white/7.5 dark:bg-[linear-gradient(180deg,rgb(255_255_255/0.022),transparent)]">
|
||||||
<div>
|
<div>
|
||||||
<p className="overline">Setup</p>
|
<p className="overline mb-1.25">Setup</p>
|
||||||
<h2>Configuration</h2>
|
<h2 className="m-0 text-[18px] font-medium text-foreground dark:text-night-heading">Configuration</h2>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="unstyled" className="secondary-button" type="button" onClick={() => setIsEditing(true)}>
|
<Button variant="unstyled" className="secondary-button" type="button" onClick={() => setIsEditing(true)}>
|
||||||
<Pencil /> Edit
|
<Pencil /> Edit
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<dl className="config-list">
|
<dl className="m-0">
|
||||||
<div className="config-row">
|
<div className="flex items-center justify-between gap-5 border-b border-border-divider p-[14px_22px] transition-colors duration-140 max-[760px]:flex-wrap dark:border-white/6.5 dark:hover:bg-white/[0.018]">
|
||||||
<dt>Method</dt>
|
<dt className="text-micro text-(--muted) dark:text-night-muted-text">Method</dt>
|
||||||
<dd>{monitor.method}</dd>
|
<dd className="m-0 font-mono text-caption/overline font-normal text-ink dark:text-night-body">{monitor.method}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div className="config-row">
|
<div className="flex items-center justify-between gap-5 border-b border-border-divider p-[14px_22px] transition-colors duration-140 max-[760px]:flex-wrap dark:border-white/6.5 dark:hover:bg-white/[0.018]">
|
||||||
<dt>Expected status</dt>
|
<dt className="text-micro text-(--muted) dark:text-night-muted-text">Expected status</dt>
|
||||||
<dd>{monitor.expectedStatus}</dd>
|
<dd className="m-0 font-mono text-caption/overline font-normal text-ink dark:text-night-body">{monitor.expectedStatus}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div className="config-row">
|
<div className="flex items-center justify-between gap-5 border-b border-border-divider p-[14px_22px] transition-colors duration-140 max-[760px]:flex-wrap dark:border-white/6.5 dark:hover:bg-white/[0.018]">
|
||||||
<dt>Check interval</dt>
|
<dt className="text-micro text-(--muted) dark:text-night-muted-text">Check interval</dt>
|
||||||
<dd>{formatInterval(monitor.intervalSeconds)}</dd>
|
<dd className="m-0 font-mono text-caption/overline font-normal text-ink dark:text-night-body">
|
||||||
|
{formatInterval(monitor.intervalSeconds)}
|
||||||
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div className="config-row">
|
<div className="flex items-center justify-between gap-5 border-b border-border-divider p-[14px_22px] transition-colors duration-140 max-[760px]:flex-wrap dark:border-white/6.5 dark:hover:bg-white/[0.018]">
|
||||||
<dt>Timeout</dt>
|
<dt className="text-micro text-(--muted) dark:text-night-muted-text">Timeout</dt>
|
||||||
<dd>{monitor.timeoutMs.toLocaleString()} ms</dd>
|
<dd className="m-0 font-mono text-caption/overline font-normal text-ink dark:text-night-body">
|
||||||
|
{monitor.timeoutMs.toLocaleString()} ms
|
||||||
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div className="config-row">
|
<div className="flex items-center justify-between gap-5 border-b border-border-divider p-[14px_22px] transition-colors duration-140 max-[760px]:flex-wrap dark:border-white/6.5 dark:hover:bg-white/[0.018]">
|
||||||
<dt>Scheduled checks</dt>
|
<dt className="text-micro text-(--muted) dark:text-night-muted-text">Scheduled checks</dt>
|
||||||
<dd className="config-row-actions">
|
<dd className="m-0 flex items-center gap-3 font-sans">
|
||||||
<span>{monitor.enabled ? 'Running' : 'Paused'}</span>
|
<Badge variant={monitor.enabled ? 'online' : 'pending'}>{monitor.enabled ? 'Running' : 'Paused'}</Badge>
|
||||||
<Button
|
<Button
|
||||||
variant="unstyled"
|
variant="outline"
|
||||||
className="row-action"
|
size="sm"
|
||||||
type="button"
|
type="button"
|
||||||
disabled={updateMutation.isPending}
|
disabled={updateMutation.isPending}
|
||||||
onClick={() => updateMutation.mutate({ id: monitor.id, input: { enabled: !monitor.enabled } })}
|
onClick={() => updateMutation.mutate({ id: monitor.id, input: { enabled: !monitor.enabled } })}
|
||||||
|
className="h-7 gap-1.5 px-2.5 font-sans text-xs font-medium text-ink transition-colors hover:border-neutral-400 dark:border-white/12 dark:bg-white/4 dark:text-night-body dark:hover:border-white/25 dark:hover:bg-white/8"
|
||||||
>
|
>
|
||||||
{monitor.enabled ? <PowerOff /> : <Power />}
|
{updateMutation.isPending ? (
|
||||||
{monitor.enabled ? 'Pause' : 'Resume'}
|
<RefreshCw className="size-3.5 animate-spin" />
|
||||||
|
) : monitor.enabled ? (
|
||||||
|
<PowerOff className="size-3.5" />
|
||||||
|
) : (
|
||||||
|
<Power className="size-3.5 text-brand-deep dark:text-brand-soft" />
|
||||||
|
)}
|
||||||
|
{updateMutation.isPending ? 'Updating…' : monitor.enabled ? 'Pause' : 'Resume'}
|
||||||
</Button>
|
</Button>
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
<div className="config-row">
|
<div className="flex items-center justify-between gap-5 p-[14px_22px] transition-colors duration-140 max-[760px]:flex-wrap dark:hover:bg-white/[0.018]">
|
||||||
<dt>Incident alerts</dt>
|
<dt className="text-micro text-(--muted) dark:text-night-muted-text">Incident alerts</dt>
|
||||||
<dd>{monitor.alertsEnabled ? 'Enabled' : 'Muted'}</dd>
|
<dd className="m-0 font-mono text-caption/overline font-normal text-ink dark:text-night-body">
|
||||||
|
{monitor.alertsEnabled ? 'Enabled' : 'Muted'}
|
||||||
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
<div className="config-danger">
|
<div className="flex items-center justify-between gap-4 border-t border-border-row bg-surface-danger-subtle p-[16px_22px] max-[760px]:flex-col max-[760px]:items-start dark:border-t-red-400/14 dark:bg-night-muted dark:bg-[linear-gradient(90deg,rgb(127_29_29/0.11),transparent_52%)]">
|
||||||
<p>This permanently deletes the monitor and its check history.</p>
|
<p className="m-0 text-micro text-(--muted) dark:text-night-muted-text">
|
||||||
|
This permanently deletes the monitor and its check history.
|
||||||
|
</p>
|
||||||
<Button variant="unstyled" className="danger-button" type="button" onClick={() => setIsConfirmingDelete(true)}>
|
<Button variant="unstyled" className="danger-button" type="button" onClick={() => setIsConfirmingDelete(true)}>
|
||||||
<Trash2 /> Delete monitor
|
<Trash2 /> Delete monitor
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
+116
-1
@@ -3,7 +3,6 @@
|
|||||||
@import 'tw-animate-css';
|
@import 'tw-animate-css';
|
||||||
@import 'shadcn/tailwind.css';
|
@import 'shadcn/tailwind.css';
|
||||||
@import './styles/StatusPage.css';
|
@import './styles/StatusPage.css';
|
||||||
@import './styles/MonitorDetailPage.css';
|
|
||||||
@import './styles/SettingsPage.css';
|
@import './styles/SettingsPage.css';
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
@@ -537,6 +536,16 @@
|
|||||||
width: 15px;
|
width: 15px;
|
||||||
height: 15px;
|
height: 15px;
|
||||||
}
|
}
|
||||||
|
.dark .back-link {
|
||||||
|
color: #858d9a;
|
||||||
|
transition: color 160ms ease;
|
||||||
|
}
|
||||||
|
.dark .back-link:hover {
|
||||||
|
color: #f3f4f6;
|
||||||
|
}
|
||||||
|
.dark .back-link svg {
|
||||||
|
color: #4ade80;
|
||||||
|
}
|
||||||
.table-empty {
|
.table-empty {
|
||||||
display: grid;
|
display: grid;
|
||||||
min-height: 150px;
|
min-height: 150px;
|
||||||
@@ -583,6 +592,16 @@
|
|||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@keyframes monitor-detail-enter {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(8px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1000px) {
|
@media (max-width: 1000px) {
|
||||||
.form-actions {
|
.form-actions {
|
||||||
@@ -661,10 +680,61 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@utility monitor-scroll {
|
||||||
|
max-height: clamp(320px, 52vh, 556px);
|
||||||
|
overflow: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
scrollbar-color: #b8c7c0 #f4f6f5;
|
||||||
|
scrollbar-gutter: stable;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid rgb(36 180 126 / 0.45);
|
||||||
|
outline-offset: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: #f4f6f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
border: 2px solid #f4f6f5;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #b8c7c0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #91a69d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark .monitor-scroll {
|
||||||
|
scrollbar-color: #3c4650 #0c0e11;
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: #0c0e11;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
border-color: #0c0e11;
|
||||||
|
background: #3c4650;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #53606b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--font-sans: 'DM Sans', 'Helvetica Neue', sans-serif;
|
--font-sans: 'DM Sans', 'Helvetica Neue', sans-serif;
|
||||||
--font-mono: 'IBM Plex Mono', monospace;
|
--font-mono: 'IBM Plex Mono', monospace;
|
||||||
--animate-enter: enter 500ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
--animate-enter: enter 500ms cubic-bezier(0.16, 1, 0.3, 1) both;
|
||||||
|
--animate-monitor-enter: monitor-detail-enter 420ms cubic-bezier(0.22, 1, 0.36, 1) both;
|
||||||
|
|
||||||
/* Brand emerald palette */
|
/* Brand emerald palette */
|
||||||
--color-brand: #3ecf8e;
|
--color-brand: #3ecf8e;
|
||||||
@@ -672,6 +742,7 @@
|
|||||||
--color-brand-hover: #36c487;
|
--color-brand-hover: #36c487;
|
||||||
--color-brand-border: #35c586;
|
--color-brand-border: #35c586;
|
||||||
--color-brand-light: #4ae09e;
|
--color-brand-light: #4ae09e;
|
||||||
|
--color-brand-soft: #4ade80;
|
||||||
--color-brand-muted: #55816e;
|
--color-brand-muted: #55816e;
|
||||||
--color-brand-dark-text: #090a0c;
|
--color-brand-dark-text: #090a0c;
|
||||||
|
|
||||||
@@ -681,6 +752,7 @@
|
|||||||
--color-faint: var(--faint);
|
--color-faint: var(--faint);
|
||||||
--color-soft: var(--soft);
|
--color-soft: var(--soft);
|
||||||
--color-night: var(--night);
|
--color-night: var(--night);
|
||||||
|
--color-primary-deep: var(--primary-deep);
|
||||||
|
|
||||||
/* Neutral text and icon shades */
|
/* Neutral text and icon shades */
|
||||||
--color-ink-label: #353535;
|
--color-ink-label: #353535;
|
||||||
@@ -689,6 +761,7 @@
|
|||||||
--color-icon-muted: #5f5f5f;
|
--color-icon-muted: #5f5f5f;
|
||||||
|
|
||||||
/* Dark mode surfaces */
|
/* Dark mode surfaces */
|
||||||
|
--color-night-base: #090b0d;
|
||||||
--color-night-root: #090a0c;
|
--color-night-root: #090a0c;
|
||||||
--color-night-card: #111419;
|
--color-night-card: #111419;
|
||||||
--color-night-input: #0b0d0f;
|
--color-night-input: #0b0d0f;
|
||||||
@@ -699,6 +772,24 @@
|
|||||||
--color-night-item-hover: #1d222b;
|
--color-night-item-hover: #1d222b;
|
||||||
--color-night-item-active: #20252e;
|
--color-night-item-active: #20252e;
|
||||||
--color-night-dialog: #0e1116;
|
--color-night-dialog: #0e1116;
|
||||||
|
--color-night-chart: #0d0f13;
|
||||||
|
--color-night-elevated: #0f1115;
|
||||||
|
--color-night-muted: #111215;
|
||||||
|
--color-night-surface: #13161b;
|
||||||
|
--color-night-card-alt: #15171b;
|
||||||
|
--color-night-incident: #151114;
|
||||||
|
|
||||||
|
/* Dark mode text tones */
|
||||||
|
--color-night-metric: #f4f7f6;
|
||||||
|
--color-night-heading: #f3f5f7;
|
||||||
|
--color-night-title: #e8ebef;
|
||||||
|
--color-night-body: #e5e7eb;
|
||||||
|
--color-night-code: #c7ced7;
|
||||||
|
--color-night-muted-text: #929aa7;
|
||||||
|
--color-night-subtle-text: #7e8794;
|
||||||
|
--color-night-faint: #737c89;
|
||||||
|
--color-night-faint-alt: #747d89;
|
||||||
|
--color-night-scale: #68717e;
|
||||||
|
|
||||||
/* Light surfaces & backgrounds */
|
/* Light surfaces & backgrounds */
|
||||||
--color-surface-soft: #fafafa;
|
--color-surface-soft: #fafafa;
|
||||||
@@ -707,6 +798,28 @@
|
|||||||
--color-surface-hover: #fdfdfd;
|
--color-surface-hover: #fdfdfd;
|
||||||
--color-surface-accent: #eef9f4;
|
--color-surface-accent: #eef9f4;
|
||||||
--color-surface-accent-hover: #e4f6ed;
|
--color-surface-accent-hover: #e4f6ed;
|
||||||
|
--color-surface-sparkline: #fdfefe;
|
||||||
|
--color-surface-uptime: #fcfefd;
|
||||||
|
--color-surface-danger-subtle: #fdfafa;
|
||||||
|
|
||||||
|
/* Status orb colors */
|
||||||
|
--color-status-online-border: #c5f1df;
|
||||||
|
--color-status-offline-border: #f2d4d4;
|
||||||
|
--color-status-offline-bg: #c74f4f;
|
||||||
|
--color-status-pending-border: #f6e2c8;
|
||||||
|
--color-status-pending-bg: #d8892f;
|
||||||
|
--color-status-pending-dark: #f0a440;
|
||||||
|
--color-status-checking-border: #eee5bd;
|
||||||
|
--color-status-checking-bg: #c6aa38;
|
||||||
|
--color-status-checking-dark: #e6c94b;
|
||||||
|
|
||||||
|
/* Incident status & stream colors */
|
||||||
|
--color-incident-border: #e8c3c3;
|
||||||
|
--color-incident-bg: #fff8f8;
|
||||||
|
--color-incident-text: #a83e3e;
|
||||||
|
--color-incident-active-bg: #fff0f0;
|
||||||
|
--color-incident-active-text: #aa4141;
|
||||||
|
--color-incident-resolved-bg: #eaf9f2;
|
||||||
|
|
||||||
/* Borders & Dividers */
|
/* Borders & Dividers */
|
||||||
--color-border-subtle: #dedede;
|
--color-border-subtle: #dedede;
|
||||||
@@ -717,6 +830,8 @@
|
|||||||
--color-border-input-hover: #aaaaaa;
|
--color-border-input-hover: #aaaaaa;
|
||||||
--color-border-accent: #c9eadb;
|
--color-border-accent: #c9eadb;
|
||||||
--color-border-accent-checked: #8dd9b9;
|
--color-border-accent-checked: #8dd9b9;
|
||||||
|
--color-border-hero: #e7e7e7;
|
||||||
|
--color-border-divider: #f0f0f0;
|
||||||
|
|
||||||
/* Text & foreground tones */
|
/* Text & foreground tones */
|
||||||
--color-ink-muted-dark: #888888;
|
--color-ink-muted-dark: #888888;
|
||||||
|
|||||||
@@ -1,879 +0,0 @@
|
|||||||
/* ==========================================================================
|
|
||||||
Monitor Detail
|
|
||||||
========================================================================== */
|
|
||||||
|
|
||||||
.detail-main {
|
|
||||||
padding-top: 34px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-hero {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 32px;
|
|
||||||
margin-top: 34px;
|
|
||||||
padding-bottom: 36px;
|
|
||||||
border-bottom: 1px solid #e7e7e7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 18px;
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-orb {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
border: 4px solid #f2d4d4;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #c74f4f;
|
|
||||||
box-shadow: 0 0 0 8px #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-orb.online {
|
|
||||||
border-color: #c5f1df;
|
|
||||||
background: var(--primary-deep);
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-orb.pending {
|
|
||||||
border-color: #f6e2c8;
|
|
||||||
background: #d8892f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-orb.checking {
|
|
||||||
border-color: #eee5bd;
|
|
||||||
background: #c6aa38;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-title h1 {
|
|
||||||
margin: 0;
|
|
||||||
overflow: hidden;
|
|
||||||
font-size: 40px;
|
|
||||||
font-weight: 500;
|
|
||||||
line-height: 1.1;
|
|
||||||
letter-spacing: -1.5px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-title a {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
max-width: min(70vw, 680px);
|
|
||||||
margin-top: 9px;
|
|
||||||
overflow: hidden;
|
|
||||||
font:
|
|
||||||
400 13px/1.4 'IBM Plex Mono',
|
|
||||||
monospace;
|
|
||||||
color: var(--muted);
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-title a:hover {
|
|
||||||
color: var(--primary-deep);
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-title a svg {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
width: 13px;
|
|
||||||
height: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.muted-alert {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.muted-alert svg {
|
|
||||||
width: 14px;
|
|
||||||
height: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sla-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(5, 1fr);
|
|
||||||
gap: 12px;
|
|
||||||
margin-top: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sla-card {
|
|
||||||
min-width: 0;
|
|
||||||
padding: 20px;
|
|
||||||
border: 1px solid #e3e3e3;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sla-card p {
|
|
||||||
margin: 0 0 17px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sla-card strong {
|
|
||||||
display: block;
|
|
||||||
font:
|
|
||||||
500 23px/1 'IBM Plex Mono',
|
|
||||||
monospace;
|
|
||||||
letter-spacing: -1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sla-card span {
|
|
||||||
display: block;
|
|
||||||
margin-top: 11px;
|
|
||||||
overflow: hidden;
|
|
||||||
font-size: 11px;
|
|
||||||
color: var(--faint);
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sla-card.incident-summary {
|
|
||||||
background: #fafafa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sla-card.incident-summary.has-incident {
|
|
||||||
border-color: #e8c3c3;
|
|
||||||
background: #fff8f8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sla-card.incident-summary.has-incident strong {
|
|
||||||
color: #a83e3e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1.55fr) minmax(300px, 0.75fr);
|
|
||||||
gap: 16px;
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-grid.lower-grid {
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lower-grid > .data-panel {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-panel {
|
|
||||||
overflow: hidden;
|
|
||||||
border: 1px solid #dedede;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-panel-heading {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 20px;
|
|
||||||
min-height: 82px;
|
|
||||||
padding: 18px 22px;
|
|
||||||
border-bottom: 1px solid #ededed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-panel-heading .overline {
|
|
||||||
margin-bottom: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-panel-heading h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-panel-heading > span {
|
|
||||||
font:
|
|
||||||
400 11px/1.4 'IBM Plex Mono',
|
|
||||||
monospace;
|
|
||||||
color: var(--faint);
|
|
||||||
}
|
|
||||||
|
|
||||||
.configuration-panel {
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-list {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-row {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 20px;
|
|
||||||
padding: 14px 22px;
|
|
||||||
border-bottom: 1px solid #f0f0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-row:last-child {
|
|
||||||
border-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-row dt {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-row dd {
|
|
||||||
margin: 0;
|
|
||||||
font:
|
|
||||||
400 13px/1.4 'IBM Plex Mono',
|
|
||||||
monospace;
|
|
||||||
color: var(--ink);
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-row .config-row-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-row-actions .row-action {
|
|
||||||
font-family: 'DM Sans', 'Helvetica Neue', sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-danger {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 16px;
|
|
||||||
padding: 16px 22px;
|
|
||||||
border-top: 1px solid #ededed;
|
|
||||||
background: #fdfafa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-danger p {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-panel,
|
|
||||||
.uptime-panel {
|
|
||||||
min-height: 330px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sparkline-wrap {
|
|
||||||
position: relative;
|
|
||||||
height: 245px;
|
|
||||||
padding: 26px 24px 24px;
|
|
||||||
background: linear-gradient(180deg, #fdfefe, #fff);
|
|
||||||
}
|
|
||||||
|
|
||||||
.chart-scale {
|
|
||||||
position: absolute;
|
|
||||||
inset: 19px 22px 20px auto;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: space-between;
|
|
||||||
pointer-events: none;
|
|
||||||
font:
|
|
||||||
400 9px/1 'IBM Plex Mono',
|
|
||||||
monospace;
|
|
||||||
color: #aaa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.uptime-bar {
|
|
||||||
height: 148px;
|
|
||||||
padding: 24px 22px 18px;
|
|
||||||
background: linear-gradient(180deg, #fcfefd 0%, #fff 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.uptime-legend {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 14px 22px 0;
|
|
||||||
border-top: 1px solid #f0f0f0;
|
|
||||||
font-size: 10px;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
|
|
||||||
.uptime-legend span {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.uptime-legend i {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--primary-deep);
|
|
||||||
}
|
|
||||||
|
|
||||||
.uptime-legend i.legend-down {
|
|
||||||
margin-left: 5px;
|
|
||||||
background: var(--chart-down);
|
|
||||||
}
|
|
||||||
|
|
||||||
.uptime-legend i.legend-degraded {
|
|
||||||
margin-left: 5px;
|
|
||||||
background: var(--chart-degraded);
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-table-wrap {
|
|
||||||
overflow-x: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recent-checks-panel {
|
|
||||||
display: flex;
|
|
||||||
min-height: 0;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recent-checks-scroll,
|
|
||||||
.incident-list {
|
|
||||||
max-height: clamp(320px, 52vh, 556px);
|
|
||||||
overflow: auto;
|
|
||||||
overscroll-behavior: contain;
|
|
||||||
scrollbar-color: #b8c7c0 #f4f6f5;
|
|
||||||
scrollbar-gutter: stable;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recent-checks-scroll:focus-visible,
|
|
||||||
.incident-list:focus-visible {
|
|
||||||
outline: 2px solid rgb(36 180 126 / 0.45);
|
|
||||||
outline-offset: -2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recent-checks-scroll::-webkit-scrollbar,
|
|
||||||
.incident-list::-webkit-scrollbar {
|
|
||||||
width: 9px;
|
|
||||||
height: 9px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recent-checks-scroll::-webkit-scrollbar-track,
|
|
||||||
.incident-list::-webkit-scrollbar-track {
|
|
||||||
background: #f4f6f5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recent-checks-scroll::-webkit-scrollbar-thumb,
|
|
||||||
.incident-list::-webkit-scrollbar-thumb {
|
|
||||||
border: 2px solid #f4f6f5;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: #b8c7c0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.recent-checks-scroll::-webkit-scrollbar-thumb:hover,
|
|
||||||
.incident-list::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: #91a69d;
|
|
||||||
}
|
|
||||||
|
|
||||||
.incident-list {
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.incident-list article {
|
|
||||||
display: flex;
|
|
||||||
gap: 13px;
|
|
||||||
padding: 18px 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.incident-list article > div {
|
|
||||||
min-width: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.incident-list article + article {
|
|
||||||
border-top: 1px solid #ededed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.incident-list article > span {
|
|
||||||
display: grid;
|
|
||||||
flex: 0 0 auto;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
place-items: center;
|
|
||||||
border-radius: 50%;
|
|
||||||
color: #16885b;
|
|
||||||
background: #eaf9f2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.incident-list article.open > span {
|
|
||||||
color: #aa4141;
|
|
||||||
background: #fff0f0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.incident-list article svg {
|
|
||||||
width: 15px;
|
|
||||||
height: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.incident-list strong {
|
|
||||||
font-size: 13px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.incident-list p {
|
|
||||||
margin: 4px 0 6px;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 1.45;
|
|
||||||
color: var(--muted);
|
|
||||||
}
|
|
||||||
|
|
||||||
.incident-list small {
|
|
||||||
font:
|
|
||||||
400 10px/1.4 'IBM Plex Mono',
|
|
||||||
monospace;
|
|
||||||
color: #aaa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.incident-list .incident-raw {
|
|
||||||
display: block;
|
|
||||||
margin: -1px 0 5px;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
|
|
||||||
.incident-detail-link {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 0;
|
|
||||||
border: 0;
|
|
||||||
color: inherit;
|
|
||||||
background: transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1000px) {
|
|
||||||
.sla-grid {
|
|
||||||
grid-template-columns: repeat(3, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-grid,
|
|
||||||
.detail-grid.lower-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
|
||||||
.detail-hero {
|
|
||||||
align-items: flex-start;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-actions {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-row {
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-danger {
|
|
||||||
align-items: flex-start;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.detail-title h1 {
|
|
||||||
font-size: 34px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sla-grid {
|
|
||||||
grid-template-columns: repeat(2, 1fr);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sla-card.incident-summary {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 520px) {
|
|
||||||
.detail-title {
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-orb {
|
|
||||||
margin-top: 11px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sla-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sla-card.incident-summary {
|
|
||||||
grid-column: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.data-panel-heading {
|
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark operations console. */
|
|
||||||
|
|
||||||
.dark .detail-main {
|
|
||||||
position: relative;
|
|
||||||
isolation: isolate;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .detail-main::before {
|
|
||||||
position: absolute;
|
|
||||||
z-index: -1;
|
|
||||||
top: -110px;
|
|
||||||
left: 50%;
|
|
||||||
width: min(920px, 92vw);
|
|
||||||
height: 430px;
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at 35% 20%, rgb(62 207 142 / 0.075), transparent 45%),
|
|
||||||
linear-gradient(90deg, rgb(255 255 255 / 0.018) 1px, transparent 1px), linear-gradient(rgb(255 255 255 / 0.018) 1px, transparent 1px);
|
|
||||||
background-size:
|
|
||||||
auto,
|
|
||||||
44px 44px,
|
|
||||||
44px 44px;
|
|
||||||
content: '';
|
|
||||||
mask-image: linear-gradient(to bottom, #000 0%, transparent 82%);
|
|
||||||
pointer-events: none;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .back-link {
|
|
||||||
color: #858d9a;
|
|
||||||
transition: color 160ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .back-link:hover {
|
|
||||||
color: #f3f4f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .back-link svg {
|
|
||||||
color: #4ade80;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .detail-hero {
|
|
||||||
border-bottom-color: rgb(255 255 255 / 0.09);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .detail-title h1 {
|
|
||||||
color: #f8fafc;
|
|
||||||
text-shadow: 0 1px 18px rgb(0 0 0 / 0.22);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .detail-title a {
|
|
||||||
color: #929aa7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .detail-title a:hover {
|
|
||||||
color: #4ade80;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .status-orb {
|
|
||||||
border-color: rgb(248 113 113 / 0.24);
|
|
||||||
background: #f87171;
|
|
||||||
box-shadow:
|
|
||||||
0 0 0 8px #090b0d,
|
|
||||||
0 0 22px rgb(248 113 113 / 0.32);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .status-orb.online {
|
|
||||||
border-color: rgb(62 207 142 / 0.22);
|
|
||||||
background: #3ecf8e;
|
|
||||||
box-shadow:
|
|
||||||
0 0 0 8px #090b0d,
|
|
||||||
0 0 24px rgb(62 207 142 / 0.38);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .status-orb.pending {
|
|
||||||
border-color: rgb(245 158 11 / 0.24);
|
|
||||||
background: #f0a440;
|
|
||||||
box-shadow:
|
|
||||||
0 0 0 8px #090b0d,
|
|
||||||
0 0 22px rgb(245 158 11 / 0.28);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .status-orb.checking {
|
|
||||||
border-color: rgb(250 204 21 / 0.22);
|
|
||||||
background: #e6c94b;
|
|
||||||
box-shadow:
|
|
||||||
0 0 0 8px #090b0d,
|
|
||||||
0 0 22px rgb(250 204 21 / 0.25);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .muted-alert {
|
|
||||||
color: #929aa7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .sla-card {
|
|
||||||
position: relative;
|
|
||||||
border-color: rgb(255 255 255 / 0.085);
|
|
||||||
background: linear-gradient(145deg, #13161b 0%, #0f1115 100%);
|
|
||||||
box-shadow:
|
|
||||||
0 10px 26px rgb(0 0 0 / 0.22),
|
|
||||||
inset 0 1px rgb(255 255 255 / 0.035);
|
|
||||||
transition:
|
|
||||||
border-color 180ms ease,
|
|
||||||
transform 180ms ease,
|
|
||||||
box-shadow 180ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .sla-card::before {
|
|
||||||
position: absolute;
|
|
||||||
top: -1px;
|
|
||||||
left: 20px;
|
|
||||||
width: 34px;
|
|
||||||
height: 1px;
|
|
||||||
background: rgb(62 207 142 / 0.65);
|
|
||||||
content: '';
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .sla-card p {
|
|
||||||
color: #929aa7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .sla-card strong {
|
|
||||||
color: #f4f7f6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .sla-card span {
|
|
||||||
color: #737c89;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .sla-card.incident-summary {
|
|
||||||
background: linear-gradient(145deg, #15171b 0%, #101216 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .sla-card.incident-summary::before {
|
|
||||||
background: rgb(148 163 184 / 0.48);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .sla-card.incident-summary.has-incident {
|
|
||||||
border-color: rgb(248 113 113 / 0.3);
|
|
||||||
background: linear-gradient(145deg, rgb(73 28 31 / 0.58), #151114 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .sla-card.incident-summary.has-incident::before {
|
|
||||||
background: #f87171;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .sla-card.incident-summary.has-incident strong {
|
|
||||||
color: #fca5a5;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (hover: hover) {
|
|
||||||
.dark .sla-card:hover {
|
|
||||||
border-color: rgb(62 207 142 / 0.24);
|
|
||||||
box-shadow:
|
|
||||||
0 14px 34px rgb(0 0 0 / 0.3),
|
|
||||||
0 0 0 1px rgb(62 207 142 / 0.035),
|
|
||||||
inset 0 1px rgb(255 255 255 / 0.05);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .data-panel {
|
|
||||||
border-color: rgb(255 255 255 / 0.085);
|
|
||||||
background: #101216;
|
|
||||||
box-shadow:
|
|
||||||
0 16px 38px rgb(0 0 0 / 0.27),
|
|
||||||
inset 0 1px rgb(255 255 255 / 0.03);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .data-panel-heading {
|
|
||||||
border-bottom-color: rgb(255 255 255 / 0.075);
|
|
||||||
background: linear-gradient(180deg, rgb(255 255 255 / 0.022), transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .data-panel-heading h2 {
|
|
||||||
color: #f3f5f7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .data-panel-heading > span {
|
|
||||||
color: #7e8794;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .sparkline-wrap,
|
|
||||||
.dark .uptime-bar {
|
|
||||||
background: linear-gradient(180deg, rgb(62 207 142 / 0.025), transparent 55%), #0d0f13;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .sparkline-wrap .recharts-cartesian-grid line {
|
|
||||||
stroke: rgb(255 255 255 / 0.075);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .chart-scale {
|
|
||||||
color: #68717e;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .monitor-chart-tooltip {
|
|
||||||
border-color: rgb(255 255 255 / 0.12);
|
|
||||||
color: #e5e7eb;
|
|
||||||
background: rgb(19 22 27 / 0.96);
|
|
||||||
box-shadow:
|
|
||||||
0 16px 40px rgb(0 0 0 / 0.55),
|
|
||||||
inset 0 1px rgb(255 255 255 / 0.045);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .uptime-legend {
|
|
||||||
border-top-color: rgb(255 255 255 / 0.065);
|
|
||||||
color: #747d89;
|
|
||||||
background: #0f1115;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .recent-checks-scroll,
|
|
||||||
.dark .incident-list {
|
|
||||||
scrollbar-color: #3c4650 #0c0e11;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .recent-checks-scroll::-webkit-scrollbar-track,
|
|
||||||
.dark .incident-list::-webkit-scrollbar-track {
|
|
||||||
background: #0c0e11;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .recent-checks-scroll::-webkit-scrollbar-thumb,
|
|
||||||
.dark .incident-list::-webkit-scrollbar-thumb {
|
|
||||||
border-color: #0c0e11;
|
|
||||||
background: #3c4650;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .recent-checks-scroll::-webkit-scrollbar-thumb:hover,
|
|
||||||
.dark .incident-list::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: #53606b;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .recent-checks-panel [data-slot='table-row'] {
|
|
||||||
transition: background-color 140ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .recent-checks-panel [data-slot='table-body'] [data-slot='table-row']:hover {
|
|
||||||
background: rgb(255 255 255 / 0.022);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .recent-checks-panel code {
|
|
||||||
color: #c7ced7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .incident-list article + article {
|
|
||||||
border-top-color: rgb(255 255 255 / 0.065);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .incident-list article {
|
|
||||||
transition: background-color 140ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .incident-list article:hover {
|
|
||||||
background: rgb(255 255 255 / 0.022);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .incident-list article > span {
|
|
||||||
color: #4ade80;
|
|
||||||
background: rgb(62 207 142 / 0.105);
|
|
||||||
box-shadow: inset 0 0 0 1px rgb(62 207 142 / 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .incident-list article.open > span {
|
|
||||||
color: #f87171;
|
|
||||||
background: rgb(248 113 113 / 0.1);
|
|
||||||
box-shadow: inset 0 0 0 1px rgb(248 113 113 / 0.14);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .incident-list strong {
|
|
||||||
color: #e8ebef;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .incident-detail-link:hover strong {
|
|
||||||
color: #4ade80;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .incident-list p {
|
|
||||||
color: #9ca3af;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .incident-list small {
|
|
||||||
color: #747d89;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .config-row {
|
|
||||||
border-bottom-color: rgb(255 255 255 / 0.065);
|
|
||||||
transition: background-color 140ms ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .config-row:hover {
|
|
||||||
background: rgb(255 255 255 / 0.018);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .config-row dt {
|
|
||||||
color: #929aa7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .config-row dd {
|
|
||||||
color: #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .config-row-actions > span {
|
|
||||||
color: #4ade80;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .config-danger {
|
|
||||||
border-top-color: rgb(248 113 113 / 0.14);
|
|
||||||
background: linear-gradient(90deg, rgb(127 29 29 / 0.11), transparent 52%), #111215;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .config-danger p {
|
|
||||||
color: #929aa7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .detail-hero,
|
|
||||||
.dark .sla-grid,
|
|
||||||
.dark .detail-grid,
|
|
||||||
.dark .configuration-panel {
|
|
||||||
animation: monitor-detail-enter 420ms both cubic-bezier(0.22, 1, 0.36, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .sla-grid {
|
|
||||||
animation-delay: 45ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .detail-grid {
|
|
||||||
animation-delay: 90ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .detail-grid.lower-grid {
|
|
||||||
animation-delay: 135ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .configuration-panel {
|
|
||||||
animation-delay: 180ms;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes monitor-detail-enter {
|
|
||||||
from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(8px);
|
|
||||||
}
|
|
||||||
to {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 520px) {
|
|
||||||
.dark .detail-main::before {
|
|
||||||
top: -80px;
|
|
||||||
height: 330px;
|
|
||||||
background-size:
|
|
||||||
auto,
|
|
||||||
34px 34px,
|
|
||||||
34px 34px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .sla-card {
|
|
||||||
padding: 18px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user