Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion e2e/incidents.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,5 +77,6 @@ test("resolved incident no longer appears in Open Incidents", async ({ page }) =
await expect(page.getByText("Resolved", { exact: true })).toBeVisible();

await page.goto("/");
await expect(page.getByRole("heading", { name: "Open Incidents" })).not.toBeVisible();
await expect(page.getByRole("heading", { name: "Open Incidents" })).toBeVisible();
await expect(page.getByText("No open incidents")).toBeVisible();
});
117 changes: 117 additions & 0 deletions src/app/incidents/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import Link from "next/link"
import { createClient } from "@/lib/supabase/server"
import type { Site, Check, Incident } from "@/lib/supabase/types"
import { formatIncidentRange } from "@/lib/format"

export const revalidate = 0

type IncidentWithSiteAndCheck = Incident & { site: Site; check: Check }

async function getAllIncidents() {
const supabase = await createClient()

const { data: incidents } = await supabase
.from("incidents")
.select("*, site:sites(*), check:checks(*)")
.order("status", { ascending: true }) // "open" before "resolved"
.order("opened_at", { ascending: false })

return (incidents ?? []) as IncidentWithSiteAndCheck[]
}

export default async function AllIncidentsPage() {
const incidents = await getAllIncidents()

return (
<main
className="max-w-[960px] mx-auto"
style={{ padding: "36px 24px 80px" }}
>
<div className="mb-7">
<Link
href="/"
className="text-sm no-underline"
style={{ color: "#C4453C" }}
>
&larr; Status
</Link>
</div>

<h1
className="text-3xl font-bold mb-6"
style={{ color: "#1A1A1A", letterSpacing: "-0.01em" }}
>
All Incidents
</h1>

<div
className="rounded overflow-hidden"
style={{ backgroundColor: "#FFFFFF", border: "1px solid #E8E4DF" }}
>
{incidents.length === 0 ? (
<div className="px-5 py-4 text-sm" style={{ color: "#5C5C5C" }}>
No incidents recorded.
</div>
) : (
incidents.map((incident, i) => {
const isOpen = incident.status === "open"
return (
<Link
key={incident.id}
href={`/incidents/${incident.id}`}
className="flex items-start gap-3.5 px-5 py-4 no-underline transition-colors"
style={{
color: "inherit",
borderTop: i > 0 ? "1px solid #F0EEEA" : undefined,
}}
>
<div className="pt-0.5 shrink-0">
<span
className="block w-2.5 h-2.5 rounded-full"
style={{
backgroundColor: isOpen ? "#C4453C" : "#A8A8A8",
}}
/>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2.5 mb-1 flex-wrap">
<span
className="text-[15px] font-semibold"
style={{ color: "#1A1A1A" }}
>
{incident.site?.name}
</span>
<span
className="text-[11px] font-bold uppercase px-2 py-0.5 rounded"
style={{
letterSpacing: "0.04em",
backgroundColor: isOpen ? "#fdecea" : "#F2F0ED",
color: isOpen ? "#C4453C" : "#807B74",
}}
>
{incident.status}
</span>
</div>
{incident.check?.error && (
<div
className="text-[13px] mb-1"
style={{ color: "#5C5C5C" }}
>
{incident.check.error}
</div>
)}
<div className="text-[13px]" style={{ color: "#807B74" }}>
{formatIncidentRange(
incident.opened_at,
incident.resolved_at
)}
</div>
</div>
</Link>
)
})
)}
</div>
</main>
)
}
52 changes: 41 additions & 11 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,14 @@ export default async function StatusPage() {
style={{ padding: "36px 24px 80px" }}
>
<RealtimeStatusPage />
{incidents.length > 0 && (
<section>
<h2
className="text-xl font-bold mb-4"
style={{ color: "#1A1A1A" }}
>
Open Incidents
</h2>
<section>
<h2
className="text-xl font-bold mb-4"
style={{ color: "#1A1A1A" }}
>
Open Incidents
</h2>
{incidents.length > 0 ? (
<div className="flex flex-col gap-3">
{incidents.map((incident) => (
<Link
Expand Down Expand Up @@ -115,10 +115,40 @@ export default async function StatusPage() {
</Link>
))}
</div>
</section>
)}
) : (
<div
className="flex items-center gap-3 rounded"
style={{
backgroundColor: "#FFFFFF",
border: "1px solid #E8E4DF",
padding: "16px 20px",
}}
>
<span
className="flex items-center justify-center w-6 h-6 rounded-full"
style={{ backgroundColor: "#E6F4EA" }}
>
<svg width="14" height="14" viewBox="0 0 14 14" fill="none">
<path d="M2.5 7.5L5.5 10.5L11.5 4" stroke="#2E7D32" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</span>
<span className="text-[15px]" style={{ color: "#5C5C5C" }}>
No open incidents
</span>
</div>
)}
<div style={{ marginTop: "12px" }}>
<Link
href="/incidents"
className="text-sm no-underline"
style={{ color: "#C4453C" }}
>
View all incidents &rarr;
</Link>
</div>
</section>

<section style={{ marginTop: incidents.length > 0 ? "44px" : "0" }}>
<section style={{ marginTop: "44px" }}>
<h2
className="text-xl font-bold mb-4"
style={{ color: "#1A1A1A" }}
Expand Down
127 changes: 127 additions & 0 deletions src/app/sites/[id]/incidents/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import Link from "next/link"
import { notFound } from "next/navigation"
import { createClient } from "@/lib/supabase/server"
import type { Site, Check, Incident } from "@/lib/supabase/types"
import { formatIncidentRange } from "@/lib/format"

export const revalidate = 0

async function getSiteIncidents(id: string) {
const supabase = await createClient()

const { data: site } = await supabase
.from("sites")
.select("*")
.eq("id", id)
.single()

if (!site) return null

const { data: incidents } = await supabase
.from("incidents")
.select("*, check:checks(*)")
.eq("site_id", id)
.order("opened_at", { ascending: false })

return {
site: site as Site,
incidents: (incidents ?? []) as (Incident & { check: Check })[],
}
}

export default async function SiteIncidentsPage({
params,
}: {
params: Promise<{ id: string }>
}) {
const { id } = await params
const data = await getSiteIncidents(id)
if (!data) notFound()

const { site, incidents } = data

return (
<main
className="max-w-[820px] mx-auto"
style={{ padding: "32px 24px 80px" }}
>
<div className="mb-7">
<Link
href={`/sites/${site.id}`}
className="text-sm no-underline"
style={{ color: "#C4453C" }}
>
&larr; {site.name}
</Link>
</div>

<h1
className="text-3xl font-bold mb-6"
style={{ color: "#1A1A1A", letterSpacing: "-0.01em" }}
>
Incidents
</h1>

<div
className="rounded overflow-hidden"
style={{ backgroundColor: "#FFFFFF", border: "1px solid #E8E4DF" }}
>
{incidents.length === 0 ? (
<div className="px-5 py-4 text-sm" style={{ color: "#5C5C5C" }}>
No incidents recorded.
</div>
) : (
incidents.map((incident, i) => {
const isOpen = incident.status === "open"
return (
<Link
key={incident.id}
href={`/incidents/${incident.id}`}
className="flex items-start gap-3.5 px-5 py-4 no-underline transition-colors"
style={{
color: "inherit",
borderTop: i > 0 ? "1px solid #F0EEEA" : undefined,
}}
>
<div className="pt-0.5 shrink-0">
<span
className="block w-2.5 h-2.5 rounded-full"
style={{
backgroundColor: isOpen ? "#C4453C" : "#A8A8A8",
}}
/>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2.5 mb-1">
<span
className="text-[15px] font-semibold"
style={{ color: "#1A1A1A" }}
>
{incident.check?.error ?? "Unknown error"}
</span>
<span
className="text-[11px] font-bold uppercase px-2 py-0.5 rounded"
style={{
letterSpacing: "0.04em",
backgroundColor: isOpen ? "#fdecea" : "#F2F0ED",
color: isOpen ? "#C4453C" : "#807B74",
}}
>
{incident.status}
</span>
</div>
<div className="text-[13px]" style={{ color: "#5C5C5C" }}>
{formatIncidentRange(
incident.opened_at,
incident.resolved_at
)}
</div>
</div>
</Link>
)
})
)}
</div>
</main>
)
}
19 changes: 14 additions & 5 deletions src/app/sites/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Link from "next/link"
import { notFound } from "next/navigation"
import { createClient } from "@/lib/supabase/server"
import type { Site, Check, Incident } from "@/lib/supabase/types"
import { formatTimeAgo } from "@/lib/format"
import { formatIncidentRange } from "@/lib/format"
import SiteFormDialog from "@/components/SiteFormDialog"
import CheckLogTable from "@/components/CheckLogTable"

Expand Down Expand Up @@ -118,7 +118,7 @@ export default async function SiteDetailPage({
No incidents recorded.
</div>
) : (
incidents.map((incident, i) => {
incidents.slice(0, 3).map((incident, i) => {
const isOpen = incident.status === "open"
return (
<Link
Expand Down Expand Up @@ -155,16 +155,25 @@ export default async function SiteDetailPage({
</span>
</div>
<div className="text-[13px]" style={{ color: "#5C5C5C" }}>
{isOpen
? `Opened ${formatTimeAgo(incident.opened_at)}`
: `${new Date(incident.opened_at).toLocaleDateString("en-US", { month: "short", day: "numeric" })} – ${new Date(incident.resolved_at!).toLocaleDateString("en-US", { month: "short", day: "numeric" })}`}
{formatIncidentRange(incident.opened_at, incident.resolved_at)}
</div>
</div>
</Link>
)
})
)}
</div>
{incidents.length > 3 && (
<div style={{ marginTop: "12px" }}>
<Link
href={`/sites/${site.id}/incidents`}
className="text-sm no-underline"
style={{ color: "#C4453C" }}
>
View all incidents &rarr;
</Link>
</div>
)}
</section>

<section>
Expand Down
Loading