From 686aeb393f178c7f380a34b71868a7d1a45bde16 Mon Sep 17 00:00:00 2001 From: Colin Ozanne Date: Thu, 4 Jun 2026 14:15:27 +0000 Subject: [PATCH] feat: integrate components from external services (#2205) * feat: integrate components from external services ci: apply automated fixes * fix: feedback * feat: per-component status pages (`/status/[id]/[component]`) * fix: linting * perf: fetch minimal history for component sibling nav * ci: apply automated fixes * fix: make one db migration * fix: move external-service-pill * ci: apply automated fixes --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../[id]/[component]/component-detail.tsx | 306 ++ .../status/[id]/[component]/page.tsx | 130 + .../src/app/(landing)/status/[id]/page.tsx | 4 + .../status/[id]/service-components.tsx | 35 + .../(landing)/status/[id]/service-detail.tsx | 9 + .../status/external-service-components.tsx | 294 ++ .../(landing)/status/external-status-grid.tsx | 3 +- .../src/app/api/og/external-service/route.tsx | 91 +- apps/web/src/app/sitemap.ts | 22 +- apps/web/src/lib/external-service-cache.ts | 17 + apps/workflows/src/cron/external-status.ts | 172 +- .../0007-store-external-service-components.md | 136 + docs/adr/README.md | 1 + packages/api/src/router/externalService.ts | 229 +- .../db/drizzle/0073_tiresome_doomsday.sql | 22 + packages/db/drizzle/meta/0073_snapshot.json | 4444 +++++++++++++++++ packages/db/drizzle/meta/_journal.json | 7 + .../external_service_component.ts | 55 + .../external_service_incident.ts | 4 + .../db/src/schema/external_services/index.ts | 1 + .../schema/external_services/validation.ts | 16 + packages/services/package.json | 4 + .../external-service-component.test.ts | 338 ++ .../__tests__/internal.test.ts | 112 + .../src/external-service-component/index.ts | 23 + .../external-service-component/internal.ts | 87 + .../src/external-service-component/list.ts | 183 + .../src/external-service-component/upsert.ts | 114 + .../external-service-incident.test.ts | 52 + .../src/external-service-incident/index.ts | 1 + .../src/external-service-incident/list.ts | 48 +- .../src/external-service-incident/upsert.ts | 3 + .../__tests__/fetchers/components.test.ts | 157 + packages/status-fetcher/__tests__/helpers.ts | 34 + packages/status-fetcher/src/components.ts | 85 + .../status-fetcher/src/fetchers/atlassian.ts | 11 + .../status-fetcher/src/fetchers/incidentio.ts | 11 + packages/status-fetcher/src/incidents.ts | 5 + packages/status-fetcher/src/index.ts | 6 + packages/status-fetcher/src/types.ts | 14 + .../external_status_component__v0.datasource | 14 + ...rnal_status_component_daily__v0.datasource | 12 + ...external_status_component_history__v0.pipe | 21 + ...__external_status_component_daily__v0.pipe | 28 + packages/tinybird/src/client.ts | 44 +- 45 files changed, 7335 insertions(+), 70 deletions(-) create mode 100644 apps/web/src/app/(landing)/status/[id]/[component]/component-detail.tsx create mode 100644 apps/web/src/app/(landing)/status/[id]/[component]/page.tsx create mode 100644 apps/web/src/app/(landing)/status/[id]/service-components.tsx create mode 100644 apps/web/src/app/(landing)/status/external-service-components.tsx create mode 100644 docs/adr/0007-store-external-service-components.md create mode 100644 packages/db/drizzle/0073_tiresome_doomsday.sql create mode 100644 packages/db/drizzle/meta/0073_snapshot.json create mode 100644 packages/db/src/schema/external_services/external_service_component.ts create mode 100644 packages/services/src/external-service-component/__tests__/external-service-component.test.ts create mode 100644 packages/services/src/external-service-component/__tests__/internal.test.ts create mode 100644 packages/services/src/external-service-component/index.ts create mode 100644 packages/services/src/external-service-component/internal.ts create mode 100644 packages/services/src/external-service-component/list.ts create mode 100644 packages/services/src/external-service-component/upsert.ts create mode 100644 packages/status-fetcher/__tests__/fetchers/components.test.ts create mode 100644 packages/status-fetcher/src/components.ts create mode 100644 packages/tinybird/datasources/external_status_component__v0.datasource create mode 100644 packages/tinybird/datasources/mv__external_status_component_daily__v0.datasource create mode 100644 packages/tinybird/endpoints/endpoint__external_status_component_history__v0.pipe create mode 100644 packages/tinybird/pipes/aggregate__external_status_component_daily__v0.pipe diff --git a/apps/web/src/app/(landing)/status/[id]/[component]/component-detail.tsx b/apps/web/src/app/(landing)/status/[id]/[component]/component-detail.tsx new file mode 100644 index 00000000..2a6f098f --- /dev/null +++ b/apps/web/src/app/(landing)/status/[id]/[component]/component-detail.tsx @@ -0,0 +1,306 @@ +"use client"; + +import { + ContentBoxDescription, + ContentBoxLink, + ContentBoxTitle, +} from "@/app/(landing)/content-box"; +import { Grid } from "@/content/mdx-components/grid"; +import { BASE_URL } from "@/lib/metadata/shared-metadata"; +import { api } from "@/trpc/rq-client"; + +import { ExternalServicePill } from "../../external-service-pill"; +import { formatRelative } from "../../utils"; +import { HistoryBars } from "../history-bars"; + +function answerFor(args: { + fullName: string; + indicator: string; + status: string; + stale: boolean; +}): string { + const { fullName, indicator, status, stale } = args; + if (stale) { + return `We don't have live data for ${fullName} right now — check the official status page below.`; + } + if (status === "under_maintenance") { + return `${fullName} is currently undergoing scheduled maintenance.`; + } + switch (indicator) { + case "none": + return `No, ${fullName} is not down. It is operational.`; + case "minor": + return `${fullName} is up, but currently experiencing a minor issue.`; + case "major": + return `Yes, ${fullName} is experiencing a partial outage right now.`; + case "critical": + return `Yes, ${fullName} is down — a major outage is currently affecting it.`; + default: + return `The current status of ${fullName} is unknown — check the official status page below.`; + } +} + +function impactClass(impact: string | undefined): string { + switch (impact) { + case "critical": + return "bg-destructive/20 text-destructive border-destructive/30"; + case "major": + return "bg-destructive/15 text-destructive border-destructive/20"; + case "minor": + return "bg-warning/15 text-warning border-warning/30"; + case "maintenance": + return "bg-info/15 text-info border-info/30"; + default: + return "bg-muted text-muted-foreground border-muted-foreground/20"; + } +} + +function formatTimestamp(value: string | null | undefined): string | null { + if (!value) return null; + const ms = Date.parse(value); + if (Number.isNaN(ms)) return null; + return new Date(ms).toLocaleString(undefined, { + dateStyle: "medium", + timeStyle: "short", + }); +} + +function jsonLd(args: { + serviceName: string; + componentName: string; + serviceUrl: string; + componentUrl: string; + answer: string; +}) { + return { + "@context": "https://schema.org", + "@graph": [ + { + "@type": "WebPage", + url: args.componentUrl, + name: `${args.serviceName} ${args.componentName} Status`, + }, + { + "@type": "FAQPage", + mainEntity: [ + { + "@type": "Question", + name: `Is ${args.serviceName} ${args.componentName} down?`, + acceptedAnswer: { "@type": "Answer", text: args.answer }, + }, + ], + }, + { + "@type": "BreadcrumbList", + itemListElement: [ + { + "@type": "ListItem", + position: 1, + name: "External Status", + item: `${BASE_URL}/status`, + }, + { + "@type": "ListItem", + position: 2, + name: args.serviceName, + item: args.serviceUrl, + }, + { + "@type": "ListItem", + position: 3, + name: args.componentName, + item: args.componentUrl, + }, + ], + }, + ], + }; +} + +function OtherComponents({ + serviceSlug, + serviceName, + currentSlug, +}: { + serviceSlug: string; + serviceName: string; + currentSlug: string; +}) { + // days:1 — this sibling nav only uses slug/name, so fetch the minimum history. + const { data } = api.externalService.components.useQuery({ + slug: serviceSlug, + days: 1, + }); + const others = + data?.components.filter((c) => c.slug !== currentSlug).slice(0, 12) ?? []; + if (others.length === 0) return null; + + return ( + <> +

Other {serviceName} components

+
+ {others.map((c) => ( + + {c.name} + + ))} +
+ + ); +} + +export function ComponentDetail({ + serviceSlug, + componentSlug, + days, +}: { + serviceSlug: string; + componentSlug: string; + days: number; +}) { + const [data] = api.externalService.component.useSuspenseQuery({ + serviceSlug, + componentSlug, + days, + }); + + if (!data.found || !data.service || !data.component) return null; + const { service, component, history, incidents } = data; + + const fullName = `${service.name} ${component.name}`; + const answer = answerFor({ + fullName, + indicator: component.indicator, + status: component.status, + stale: component.stale, + }); + + const serviceUrl = `${BASE_URL}/status/${service.slug}`; + const componentUrl = `${serviceUrl}/${component.slug}`; + const ld = jsonLd({ + serviceName: service.name, + componentName: component.name, + serviceUrl, + componentUrl, + answer, + }); + + return ( +
+