Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export function HelpAndFeedback({
button={
<PopoverTrigger
className={cn(
"group flex h-8 items-center gap-1.5 rounded pl-[0.4375rem] pr-2 transition-colors hover:bg-charcoal-750",
"group flex h-8 items-center gap-1.5 rounded pl-[0.4375rem] pr-2 transition-colors hover:bg-charcoal-750 focus-custom",
isCollapsed ? "w-full" : "w-full justify-between"
)}
>
Expand Down
121 changes: 62 additions & 59 deletions apps/webapp/app/routes/resources.incidents.tsx
Original file line number Diff line number Diff line change
@@ -1,58 +1,87 @@
import { ExclamationTriangleIcon } from "@heroicons/react/20/solid";
import { json } from "@remix-run/node";
import { useFetcher } from "@remix-run/react";
import { useFetcher, type ShouldRevalidateFunction } from "@remix-run/react";
import { motion } from "framer-motion";
import { useCallback, useEffect } from "react";
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 } from "~/services/betterstack/betterstack.server";
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.getIncidents();
const result = await client.getIncidentStatus();

if (!result.success) {
return json({ operational: true });
return json<IncidentLoaderData>({ status: "operational", title: null });
}

return json({
operational: result.data.attributes.aggregate_state === "operational",
return json<IncidentLoaderData>({
status: result.data.status,
title: result.data.title,
});
}

export function IncidentStatusPanel({ isCollapsed = false }: { isCollapsed?: boolean }) {
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 fetchIncidents = useCallback(() => {
if (fetcher.state === "idle") {
fetcher.load("/resources/incidents");
}
}, []);
const hasInitiallyFetched = useRef(false);

useEffect(() => {
if (!isManagedCloud) return;

fetchIncidents();
// Initial fetch on mount
if (!hasInitiallyFetched.current && fetcher.state === "idle") {
hasInitiallyFetched.current = true;
fetcher.load("/resources/incidents");
}

const interval = setInterval(fetchIncidents, 60 * 1000); // 1 minute
// Poll every 60 seconds
const interval = setInterval(() => {
if (fetcher.state === "idle") {
fetcher.load("/resources/incidents");
}
}, POLL_INTERVAL_MS);

return () => clearInterval(interval);
}, [isManagedCloud, fetchIncidents]);
}, [isManagedCloud]);

return {
status: fetcher.data?.status ?? "operational",
title: fetcher.data?.title ?? null,
hasIncident: (fetcher.data?.status ?? "operational") !== "operational",
isManagedCloud,
};
}

const operational = fetcher.data?.operational ?? true;
export function IncidentStatusPanel({ isCollapsed = false }: { isCollapsed?: boolean }) {
const { title, hasIncident, isManagedCloud } = useIncidentStatus();

if (!isManagedCloud || operational) {
if (!isManagedCloud || !hasIncident) {
return null;
}

const message = title || DEFAULT_MESSAGE;

return (
<Popover>
<div className="p-1">
{/* Expanded panel - animated height and opacity */}
<motion.div
initial={false}
animate={{
Expand All @@ -62,35 +91,9 @@ export function IncidentStatusPanel({ isCollapsed = false }: { isCollapsed?: boo
transition={{ duration: 0.15 }}
className="overflow-hidden"
>
<div className="flex flex-col gap-2 rounded border border-warning/20 bg-warning/5 p-2 pt-1.5">
{/* Header */}
<div className="flex items-center gap-1 border-b border-warning/20 pb-1 text-warning">
<ExclamationTriangleIcon className="size-4" />
<Paragraph variant="small/bright" className="text-warning">
Active incident
</Paragraph>
</div>

{/* Description */}
<Paragraph variant="extra-small/bright" className="text-warning/80">
Our team is working on resolving the issue. Check our status page for more
information.
</Paragraph>

{/* Button */}
<LinkButton
variant="secondary/small"
to="https://status.trigger.dev"
target="_blank"
fullWidth
className="border-warning/20 bg-warning/10 hover:!border-warning/30 hover:!bg-warning/20"
>
<span className="text-warning">View status page</span>
</LinkButton>
</div>
<IncidentPanelContent message={message} />
</motion.div>

{/* Collapsed button - animated height and opacity */}
<motion.div
initial={false}
animate={{
Expand All @@ -102,8 +105,8 @@ export function IncidentStatusPanel({ isCollapsed = false }: { isCollapsed?: boo
>
<SimpleTooltip
button={
<PopoverTrigger className="flex !h-8 w-full items-center justify-center rounded border border-warning/20 bg-warning/10 transition-colors hover:border-warning/30 hover:bg-warning/20">
<ExclamationTriangleIcon className="size-5 text-warning" />
<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"
Expand All @@ -115,32 +118,32 @@ export function IncidentStatusPanel({ isCollapsed = false }: { isCollapsed?: boo
</motion.div>
</div>
<PopoverContent side="right" sideOffset={8} align="start" className="!min-w-0 w-52 p-0">
<IncidentPopoverContent />
<IncidentPanelContent message={message} />
</PopoverContent>
</Popover>
);
}

function IncidentPopoverContent() {
function IncidentPanelContent({ message }: { message: string }) {
return (
<div className="flex flex-col gap-2 rounded border border-warning/20 bg-warning/5 p-2 pt-1.5">
<div className="flex items-center gap-1 border-b border-warning/20 pb-1 text-warning">
<ExclamationTriangleIcon className="size-4" />
<Paragraph variant="small/bright" className="text-warning">
<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-warning/80">
Our team is working on resolving the issue. Check our status page for more information.
<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-warning/20 bg-warning/10 hover:!border-warning/30 hover:!bg-warning/20"
className="border-yellow-500/30 bg-yellow-500/15 hover:!border-yellow-500/50 hover:!bg-yellow-500/25"
>
<span className="text-warning">View status page</span>
<span className="text-yellow-300">View status page</span>
</LinkButton>
</div>
);
Expand Down
Loading