chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user