159 lines
4.9 KiB
TypeScript
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>
|
|
);
|
|
}
|