Skip to content

Commit fce1ef7

Browse files
committed
WIP adding a side menu panel to display incident statuses
1 parent 2b586c8 commit fce1ef7

File tree

4 files changed

+149
-12
lines changed

4 files changed

+149
-12
lines changed

apps/webapp/app/components/navigation/SideMenu.tsx

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
ServerStackIcon,
1818
Squares2X2Icon,
1919
} from "@heroicons/react/20/solid";
20-
import { useLocation, useNavigation } from "@remix-run/react";
20+
import { useNavigation } from "@remix-run/react";
2121
import { useEffect, useRef, useState, type ReactNode } from "react";
2222
import simplur from "simplur";
2323
import { AISparkleIcon } from "~/assets/icons/AISparkleIcon";
@@ -31,6 +31,7 @@ import { type MatchedProject } from "~/hooks/useProject";
3131
import { type User } from "~/models/user.server";
3232
import { useCurrentPlan } from "~/routes/_app.orgs.$organizationSlug/route";
3333
import { type FeedbackType } from "~/routes/resources.feedback";
34+
import { IncidentStatusPanel } from "~/routes/resources.incidents";
3435
import { cn } from "~/utils/cn";
3536
import {
3637
accountPath,
@@ -279,16 +280,19 @@ export function SideMenu({
279280
</SideMenuSection>
280281
</div>
281282
</div>
282-
<div className="flex flex-col gap-1 border-t border-grid-bright p-1">
283-
<div className="flex w-full items-center justify-between">
284-
<HelpAndAI />
283+
<div>
284+
<IncidentStatusPanel />
285+
<div className="flex flex-col gap-1 border-t border-grid-bright p-1">
286+
<div className="flex w-full items-center justify-between">
287+
<HelpAndAI />
288+
</div>
289+
{isFreeUser && (
290+
<FreePlanUsage
291+
to={v3BillingPath(organization)}
292+
percentage={currentPlan.v3Usage.usagePercentage}
293+
/>
294+
)}
285295
</div>
286-
{isFreeUser && (
287-
<FreePlanUsage
288-
to={v3BillingPath(organization)}
289-
percentage={currentPlan.v3Usage.usagePercentage}
290-
/>
291-
)}
292296
</div>
293297
</div>
294298
);

apps/webapp/app/env.server.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { SecretStoreOptionsSchema } from "./services/secrets/secretStoreOptionsSchema.server";
21
import { z } from "zod";
3-
import { isValidRegex } from "./utils/regex";
2+
import { SecretStoreOptionsSchema } from "./services/secrets/secretStoreOptionsSchema.server";
43
import { isValidDatabaseUrl } from "./utils/db";
4+
import { isValidRegex } from "./utils/regex";
55

66
const EnvironmentSchema = z.object({
77
NODE_ENV: z.union([z.literal("development"), z.literal("production"), z.literal("test")]),
@@ -721,6 +721,10 @@ const EnvironmentSchema = z.object({
721721

722722
// kapa.ai
723723
KAPA_AI_WEBSITE_ID: z.string().optional(),
724+
725+
// BetterStack
726+
BETTERSTACK_API_KEY: z.string().optional(),
727+
BETTERSTACK_STATUS_PAGE_ID: z.string().optional(),
724728
});
725729

726730
export type Environment = z.infer<typeof EnvironmentSchema>;
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { ExclamationTriangleIcon } from "@heroicons/react/20/solid";
2+
import { json } from "@remix-run/node";
3+
import { useFetcher } from "@remix-run/react";
4+
import { useCallback, useEffect } from "react";
5+
import { LinkButton } from "~/components/primitives/Buttons";
6+
import { Paragraph } from "~/components/primitives/Paragraph";
7+
import { useFeatures } from "~/hooks/useFeatures";
8+
import { BetterStackClient } from "~/services/betterstack/betterstack.server";
9+
10+
export async function loader() {
11+
const client = new BetterStackClient();
12+
const result = await client.getIncidents();
13+
14+
if (!result.success) {
15+
return json({ operational: true });
16+
}
17+
18+
return json({
19+
operational: result.data.data.attributes.aggregate_state === "operational",
20+
});
21+
}
22+
23+
export function IncidentStatusPanel() {
24+
const { isManagedCloud } = useFeatures();
25+
if (!isManagedCloud) {
26+
return null;
27+
}
28+
29+
const fetcher = useFetcher<typeof loader>();
30+
31+
const fetchIncidents = useCallback(() => {
32+
if (fetcher.state === "idle") {
33+
fetcher.load("/resources/incidents");
34+
}
35+
}, [fetcher]);
36+
37+
useEffect(() => {
38+
fetchIncidents();
39+
40+
const interval = setInterval(fetchIncidents, 60 * 1000); // 1 minute
41+
42+
return () => clearInterval(interval);
43+
}, [fetchIncidents]);
44+
45+
const operational = fetcher.data?.operational ?? true;
46+
47+
return (
48+
<>
49+
{!operational && (
50+
<div className="p-1">
51+
<div className="flex flex-col gap-2 rounded border border-warning/20 bg-warning/5 p-2 pt-1.5">
52+
<div className="flex items-center gap-1 border-b border-warning/20 pb-1 text-warning">
53+
<ExclamationTriangleIcon className="size-4" />
54+
<Paragraph variant="small/bright" className="text-warning">
55+
Active Incident
56+
</Paragraph>
57+
</div>
58+
<Paragraph variant="extra-small/bright" className="line-clamp-3 text-warning/80">
59+
We're currently experiencing service disruptions. Our team is actively working on
60+
resolving the issue. Check our status page for real-time updates.
61+
</Paragraph>
62+
<LinkButton
63+
variant="secondary/small"
64+
to="https://status.trigger.dev"
65+
target="_blank"
66+
fullWidth
67+
>
68+
View Status Page
69+
</LinkButton>
70+
</div>
71+
</div>
72+
)}
73+
</>
74+
);
75+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { wrapZodFetch } from "@trigger.dev/core/v3/zodfetch";
2+
import { z } from "zod";
3+
import { env } from "~/env.server";
4+
5+
const IncidentSchema = z.object({
6+
data: z.object({
7+
id: z.string(),
8+
type: z.string(),
9+
attributes: z.object({
10+
aggregate_state: z.string(),
11+
}),
12+
}),
13+
});
14+
15+
export type Incident = z.infer<typeof IncidentSchema>;
16+
17+
export class BetterStackClient {
18+
private readonly baseUrl = "https://uptime.betterstack.com/api/v2";
19+
20+
async getIncidents() {
21+
const apiKey = env.BETTERSTACK_API_KEY;
22+
if (!apiKey) {
23+
return { success: false as const, error: "BETTERSTACK_API_KEY is not set" };
24+
}
25+
26+
const statusPageId = env.BETTERSTACK_STATUS_PAGE_ID;
27+
if (!statusPageId) {
28+
return { success: false as const, error: "BETTERSTACK_STATUS_PAGE_ID is not set" };
29+
}
30+
31+
try {
32+
return await wrapZodFetch(
33+
IncidentSchema,
34+
`${this.baseUrl}/status-pages/${statusPageId}`,
35+
{
36+
headers: {
37+
Authorization: `Bearer ${apiKey}`,
38+
"Content-Type": "application/json",
39+
},
40+
},
41+
{
42+
retry: {
43+
maxAttempts: 3,
44+
minTimeoutInMs: 1000,
45+
maxTimeoutInMs: 5000,
46+
},
47+
}
48+
);
49+
} catch (error) {
50+
console.error("Failed to fetch incidents from BetterStack:", error);
51+
return { success: false as const, error };
52+
}
53+
}
54+
}

0 commit comments

Comments
 (0)