Skip to content

Commit 6b25850

Browse files
atrakhConvex, Inc.
authored andcommitted
dashboard: add system status notifications to Header (#44269)
Adds a status widget that displays any ongoing incidents in the dashboard header. For the Header, it will only render if there's an incident, but the underlying component supports rendering green statuses for other uses cases (like in the upcoming "Deployment disconnected" overlay GitOrigin-RevId: 53b8c0aac46afd33d85690d33b4b183229293e02
1 parent c6df656 commit 6b25850

File tree

7 files changed

+223
-3
lines changed

7 files changed

+223
-3
lines changed

npm-packages/dashboard-common/src/features/health/components/ChartForFunctionRate.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { DeploymentTimes } from "@common/features/health/components/DeploymentTi
2020
import { Button } from "@ui/Button";
2121
import { FunctionNameOption } from "@common/elements/FunctionNameOption";
2222
import { LoadingTransition } from "@ui/Loading";
23+
import { Spinner } from "@ui/Spinner";
2324

2425
export function ChartForFunctionRate({
2526
chartData,
@@ -49,8 +50,14 @@ export function ChartForFunctionRate({
4950
<LoadingTransition
5051
loadingProps={{
5152
fullHeight: false,
52-
className: "h-[9rem] w-full",
53+
className: "h-full w-full",
54+
shimmer: false,
5355
}}
56+
loadingState={
57+
<div className="flex h-full w-full items-center justify-center">
58+
<Spinner className="size-12" />
59+
</div>
60+
}
5461
>
5562
{chartData === null ? (
5663
<div className="flex h-[11.25rem] w-full items-center justify-center px-12 text-center text-sm text-content-secondary">

npm-packages/dashboard/src/api/offlineNotification.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ export const createOfflineToastContent = () => (
3030
* @param error Optional error to capture in Sentry
3131
*/
3232
export const showOfflineToast = (error?: Error) => {
33-
toast("error", createOfflineToastContent(), OFFLINE_TOAST_ID);
33+
toast("error", createOfflineToastContent(), OFFLINE_TOAST_ID, false);
3434

3535
// Log the error to Sentry if provided
3636
if (error) {

npm-packages/dashboard/src/components/header/Header/Header.tsx

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { AskAI } from "elements/AskAI";
99
import { DeploymentDisplay } from "elements/DeploymentDisplay";
1010
import { useCurrentProject } from "api/projects";
1111
import { User } from "@workos-inc/node";
12+
import { ConvexStatusWidget } from "lib/ConvexStatusWidget";
13+
import { useConvexStatus } from "hooks/useConvexStatus";
1214
import { UserMenu } from "../UserMenu/UserMenu";
1315

1416
type HeaderProps = {
@@ -17,6 +19,21 @@ type HeaderProps = {
1719
user: User | null;
1820
};
1921

22+
function ConvexStatus() {
23+
const { status } = useConvexStatus();
24+
25+
// Only show if there are issues (not operational) or still loading
26+
if (status && status.indicator === "none") {
27+
return null;
28+
}
29+
30+
return (
31+
<div className="flex items-center px-2.5">
32+
<ConvexStatusWidget status={status} />
33+
</div>
34+
);
35+
}
36+
2037
function Support() {
2138
const [openState, setOpenState] = useSupportFormOpen();
2239
return (
@@ -67,7 +84,8 @@ export function Header({ children, logoLink = "/", user }: HeaderProps) {
6784
</div>
6885
{project && <DeploymentDisplay project={project} />}
6986
<div className="flex items-center bg-background-secondary px-2">
70-
<div className="flex">
87+
<div className="flex items-center">
88+
<ConvexStatus />
7189
<AskAI />
7290
<Support />
7391
</div>
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import useSWR from "swr";
2+
import type {
3+
ConvexStatus,
4+
ConvexStatusIndicator,
5+
} from "lib/ConvexStatusWidget";
6+
7+
export type { ConvexStatus, ConvexStatusIndicator };
8+
9+
interface ConvexStatusResponse {
10+
status: {
11+
indicator: ConvexStatusIndicator;
12+
description: string;
13+
};
14+
}
15+
16+
/**
17+
* Hook to poll the Convex status page API and get current status information.
18+
* Polls every 30 seconds and on window focus (throttled to 30 seconds).
19+
*/
20+
export function useConvexStatus(): {
21+
status: ConvexStatus | undefined;
22+
} {
23+
const { data } = useSWR<ConvexStatusResponse>("/api/status", {
24+
refreshInterval: 1000 * 30,
25+
focusThrottleInterval: 1000 * 30,
26+
shouldRetryOnError: false,
27+
fetcher: convexStatusFetcher,
28+
});
29+
30+
return {
31+
status: data?.status,
32+
};
33+
}
34+
35+
const convexStatusFetcher = async (
36+
url: string,
37+
): Promise<ConvexStatusResponse> => {
38+
const res = await fetch(url);
39+
if (!res.ok) {
40+
throw new Error("Failed to fetch Convex status information.");
41+
}
42+
return res.json();
43+
};
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type { Meta, StoryObj } from "@storybook/nextjs";
2+
import { ConvexStatusWidget } from "./ConvexStatusWidget";
3+
4+
const meta: Meta<typeof ConvexStatusWidget> = {
5+
component: ConvexStatusWidget,
6+
parameters: {
7+
layout: "padded",
8+
},
9+
};
10+
11+
export default meta;
12+
type Story = StoryObj<typeof meta>;
13+
14+
export const AllOperational: Story = {
15+
args: {
16+
status: {
17+
indicator: "none",
18+
description: "All Systems Operational",
19+
},
20+
},
21+
};
22+
23+
export const MinorIssues: Story = {
24+
args: {
25+
status: {
26+
indicator: "minor",
27+
description: "Minor Service Disruption",
28+
},
29+
},
30+
};
31+
32+
export const MajorOutage: Story = {
33+
args: {
34+
status: {
35+
indicator: "major",
36+
description: "Major Service Outage",
37+
},
38+
},
39+
};
40+
41+
export const CriticalOutage: Story = {
42+
args: {
43+
status: {
44+
indicator: "critical",
45+
description: "Critical System Failure",
46+
},
47+
},
48+
};
49+
50+
export const Loading: Story = {
51+
args: {
52+
status: undefined,
53+
},
54+
};
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { ExternalLinkIcon } from "@radix-ui/react-icons";
2+
import { cn } from "@ui/cn";
3+
import { Spinner } from "@ui/Spinner";
4+
5+
export type ConvexStatusIndicator = "none" | "minor" | "major" | "critical";
6+
7+
export interface ConvexStatus {
8+
indicator: ConvexStatusIndicator;
9+
description: string;
10+
}
11+
12+
const STATUS_PAGE_URL = "https://status.convex.dev";
13+
14+
export function ConvexStatusWidget({ status }: { status?: ConvexStatus }) {
15+
return (
16+
<a
17+
href={STATUS_PAGE_URL}
18+
target="_blank"
19+
rel="noreferrer"
20+
className={cn(
21+
"flex items-center gap-2 text-sm hover:underline",
22+
!status && "animate-fadeInFromLoading",
23+
)}
24+
>
25+
{!status ? (
26+
<>
27+
<div>
28+
<Spinner className="animate-fadeInFromLoading" />
29+
</div>
30+
<span className="animate-fadeInFromLoading text-content-secondary">
31+
Loading system status...
32+
</span>
33+
<ExternalLinkIcon className="animate-fadeInFromLoading" />
34+
</>
35+
) : (
36+
<>
37+
<span className="relative flex size-3 shrink-0">
38+
{status.indicator === "none" && (
39+
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-content-success opacity-75" />
40+
)}
41+
<span
42+
className={cn(
43+
"relative inline-flex size-3 rounded-full",
44+
// eslint-disable-next-line no-restricted-syntax
45+
status.indicator === "none" && "bg-content-success",
46+
status.indicator === "minor" && "bg-yellow-500",
47+
// eslint-disable-next-line no-restricted-syntax
48+
status.indicator === "major" && "bg-content-errorSecondary",
49+
// eslint-disable-next-line no-restricted-syntax
50+
status.indicator === "critical" && "bg-content-errorSecondary",
51+
)}
52+
/>
53+
</span>
54+
<span className="flex items-center gap-1">
55+
{status.description}
56+
<ExternalLinkIcon />
57+
</span>
58+
</>
59+
)}
60+
</a>
61+
);
62+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import type { NextApiRequest, NextApiResponse } from "next";
2+
3+
type ConvexStatusIndicator = "none" | "minor" | "major" | "critical";
4+
5+
interface ConvexStatusResponse {
6+
status: {
7+
indicator: ConvexStatusIndicator;
8+
description: string;
9+
};
10+
}
11+
12+
export default async function handler(
13+
request: NextApiRequest,
14+
response: NextApiResponse<ConvexStatusResponse | { error: string }>,
15+
) {
16+
try {
17+
const statusResponse = await fetch(
18+
"https://status.convex.dev/api/v2/status.json",
19+
{
20+
method: "GET",
21+
},
22+
);
23+
24+
if (!statusResponse.ok) {
25+
response.status(500).json({ error: "Failed to fetch Convex status" });
26+
return;
27+
}
28+
29+
const statusData: ConvexStatusResponse = await statusResponse.json();
30+
31+
response.status(200).json(statusData);
32+
} catch (error) {
33+
console.error("Error fetching Convex status:", error);
34+
response.status(500).json({ error: "Failed to fetch Convex status" });
35+
}
36+
}

0 commit comments

Comments
 (0)