Files
2026-07-13 13:32:57 +08:00

159 lines
4.9 KiB
TypeScript

import { ExclamationTriangleIcon } from "@heroicons/react/20/solid";
import { json } from "@remix-run/node";
import { useFetcher, type ShouldRevalidateFunction } from "@remix-run/react";
import { motion } from "framer-motion";
import { useEffect, useRef } from "react";
import { LinkButton } from "~/components/primitives/Buttons";
import { Paragraph } from "~/components/primitives/Paragraph";
import { Popover, PopoverContent, PopoverTrigger } from "~/components/primitives/Popover";
import { SimpleTooltip } from "~/components/primitives/Tooltip";
import { useFeatures } from "~/hooks/useFeatures";
import { BetterStackClient, type AggregateState } from "~/services/betterstack/betterstack.server";
// Prevent Remix from revalidating this route when other fetchers submit
export const shouldRevalidate: ShouldRevalidateFunction = () => false;
export type IncidentLoaderData = {
status: AggregateState;
title: string | null;
};
export async function loader() {
const client = new BetterStackClient();
const result = await client.getIncidentStatus();
if (!result.success) {
return json<IncidentLoaderData>({ status: "operational", title: null });
}
return json<IncidentLoaderData>({
status: result.data.status,
title: result.data.title,
});
}
const DEFAULT_MESSAGE =
"Our team is working on resolving the issue. Check our status page for more information.";
const POLL_INTERVAL_MS = 60_000;
/** Hook to fetch and poll incident status */
export function useIncidentStatus() {
const { isManagedCloud } = useFeatures();
const fetcher = useFetcher<typeof loader>();
const hasInitiallyFetched = useRef(false);
useEffect(() => {
if (!isManagedCloud) return;
// Initial fetch on mount
if (!hasInitiallyFetched.current && fetcher.state === "idle") {
hasInitiallyFetched.current = true;
fetcher.load("/resources/incidents");
}
// Poll every 60 seconds
const interval = setInterval(() => {
if (fetcher.state === "idle") {
fetcher.load("/resources/incidents");
}
}, POLL_INTERVAL_MS);
return () => clearInterval(interval);
}, [isManagedCloud]);
return {
status: fetcher.data?.status ?? "operational",
title: fetcher.data?.title ?? null,
hasIncident: (fetcher.data?.status ?? "operational") !== "operational",
isManagedCloud,
};
}
export function IncidentStatusPanel({
isCollapsed = false,
title,
hasIncident,
isManagedCloud,
}: {
isCollapsed?: boolean;
title: string | null;
hasIncident: boolean;
isManagedCloud: boolean;
}) {
if (!isManagedCloud || !hasIncident) {
return null;
}
const message = title || DEFAULT_MESSAGE;
return (
<Popover>
<div className="p-1">
<motion.div
initial={false}
animate={{
height: isCollapsed ? 0 : "auto",
opacity: isCollapsed ? 0 : 1,
}}
transition={{ duration: 0.15 }}
className="overflow-hidden"
>
<IncidentPanelContent message={message} />
</motion.div>
<motion.div
initial={false}
animate={{
height: isCollapsed ? "auto" : 0,
opacity: isCollapsed ? 1 : 0,
}}
transition={{ duration: 0.15 }}
className="overflow-hidden"
>
<SimpleTooltip
button={
<PopoverTrigger className="flex h-8! w-full items-center justify-center rounded border border-yellow-500/30 bg-yellow-500/15 transition-colors hover:border-yellow-500/50 hover:bg-yellow-500/25">
<ExclamationTriangleIcon className="size-5 text-yellow-400" />
</PopoverTrigger>
}
content="Active incident"
side="right"
sideOffset={8}
disableHoverableContent
asChild
/>
</motion.div>
</div>
<PopoverContent side="right" sideOffset={8} align="start" className="min-w-0! w-52 p-0">
<IncidentPanelContent message={message} />
</PopoverContent>
</Popover>
);
}
function IncidentPanelContent({ message }: { message: string }) {
return (
<div className="flex flex-col gap-2 rounded border border-yellow-500/30 bg-yellow-500/10 p-2 pt-1.5">
<div className="flex items-center gap-1 border-b border-yellow-500/30 pb-1">
<ExclamationTriangleIcon className="size-4 text-yellow-400" />
<Paragraph variant="small/bright" className="text-yellow-300">
Active incident
</Paragraph>
</div>
<Paragraph variant="extra-small/bright" className="text-yellow-300">
{message}
</Paragraph>
<LinkButton
variant="secondary/small"
to="https://status.trigger.dev"
target="_blank"
fullWidth
className="border-yellow-500/30 bg-yellow-500/15 hover:border-yellow-500/50! hover:bg-yellow-500/25!"
>
<span className="text-yellow-300">View status page</span>
</LinkButton>
</div>
);
}