diff --git a/keep-ui/app/(keep)/error.css b/keep-ui/app/(keep)/error.css
deleted file mode 100644
index 8ea2ff3918..0000000000
--- a/keep-ui/app/(keep)/error.css
+++ /dev/null
@@ -1,25 +0,0 @@
-.error-container {
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- height: 100%;
- text-align: center;
-}
-
-.error-message {
- font-size: 18px;
- margin-bottom: 16px;
-}
-
-.error-url {
- font-size: 14px;
- margin-bottom: 8px;
- color: gray;
-}
-
-.error-image {
- margin-top: 16px;
- width: 150px;
- height: 150px;
-}
diff --git a/keep-ui/app/(keep)/error.ts b/keep-ui/app/(keep)/error.ts
new file mode 100644
index 0000000000..dc011a6e45
--- /dev/null
+++ b/keep-ui/app/(keep)/error.ts
@@ -0,0 +1,5 @@
+"use client";
+
+import { ErrorComponent } from "@/shared/ui";
+
+export default ErrorComponent;
diff --git a/keep-ui/app/(keep)/error.tsx b/keep-ui/app/(keep)/error.tsx
deleted file mode 100644
index 1de50c4599..0000000000
--- a/keep-ui/app/(keep)/error.tsx
+++ /dev/null
@@ -1,79 +0,0 @@
-// The error.js file convention allows you to gracefully handle unexpected runtime errors.
-// The way it does this is by automatically wrap a route segment and its nested children in a React Error Boundary.
-// https://nextjs.org/docs/app/api-reference/file-conventions/error
-// https://nextjs.org/docs/app/building-your-application/routing/error-handling#how-errorjs-works
-
-"use client";
-import Image from "next/image";
-import "./error.css";
-import { useEffect } from "react";
-import { Title, Subtitle } from "@tremor/react";
-import { Button, Text } from "@tremor/react";
-import { KeepApiError } from "@/shared/api";
-import * as Sentry from "@sentry/nextjs";
-import { useSignOut } from "@/shared/lib/hooks/useSignOut";
-
-export default function ErrorComponent({
- error,
- reset,
-}: {
- error: Error | KeepApiError;
- reset: () => void;
-}) {
- const signOut = useSignOut();
-
- useEffect(() => {
- Sentry.captureException(error);
- }, [error]);
-
- return (
-
-
- {error instanceof KeepApiError
- ? "An error occurred while fetching data from the backend"
- : error.message || "An error occurred"}
-
-
-
- {error instanceof KeepApiError && (
-
- Status Code: {error.statusCode}
-
- Message: {error.message}
-
- URL: {error.url}
-
- )}
-
-
- {error instanceof KeepApiError && error.proposedResolution && (
-
{error.proposedResolution}
- )}
-
-
-
-
- {error instanceof KeepApiError && error.statusCode === 401 ? (
-
- ) : (
-
- )}
-
- );
-}
diff --git a/keep-ui/app/(keep)/loading.tsx b/keep-ui/app/(keep)/loading.tsx
index c56c5d8ac9..d05bf61fdd 100644
--- a/keep-ui/app/(keep)/loading.tsx
+++ b/keep-ui/app/(keep)/loading.tsx
@@ -17,7 +17,7 @@ export default function Loading({
}`}
>
;
+ if (error) {
+ return ;
+ }
+
+ if ((!users || !roles || !groups) && !isLoading) {
+ return ;
+ }
const handleRowClick = (user: User) => {
setSelectedUser(user);
diff --git a/keep-ui/app/(keep)/workflows/workflows.client.tsx b/keep-ui/app/(keep)/workflows/workflows.client.tsx
index 1e4b2d0d13..7a18497946 100644
--- a/keep-ui/app/(keep)/workflows/workflows.client.tsx
+++ b/keep-ui/app/(keep)/workflows/workflows.client.tsx
@@ -2,17 +2,16 @@
import { useRef, useState } from "react";
import useSWR from "swr";
-import { Callout, Subtitle } from "@tremor/react";
+import { Subtitle } from "@tremor/react";
import {
ArrowUpOnSquareStackIcon,
- ExclamationCircleIcon,
PlusCircleIcon,
} from "@heroicons/react/24/outline";
import { Workflow, MockWorkflow } from "./models";
import Loading from "@/app/(keep)/loading";
import React from "react";
import WorkflowsEmptyState from "./noworkflows";
-import WorkflowTile, { WorkflowTileOld } from "./workflow-tile";
+import WorkflowTile from "./workflow-tile";
import { Button, Card, Title } from "@tremor/react";
import { ArrowRightIcon } from "@radix-ui/react-icons";
import { useRouter } from "next/navigation";
@@ -20,7 +19,7 @@ import Modal from "@/components/ui/Modal";
import MockWorkflowCardSection from "./mockworkflows";
import { useApi } from "@/shared/lib/hooks/useApi";
import { KeepApiError } from "@/shared/api";
-import { showErrorToast, Input } from "@/shared/ui";
+import { showErrorToast, Input, ErrorComponent } from "@/shared/ui";
export default function WorkflowsPage() {
const api = useApi();
@@ -62,19 +61,12 @@ export default function WorkflowsPage() {
(url: string) => api.get(url)
);
- if (isLoading || !data) return ;
-
if (error) {
- return (
-
- Failed to load workflows
-
- );
+ return {}} />;
+ }
+
+ if (isLoading || !data) {
+ return ;
}
const onDrop = async (files: any) => {
diff --git a/keep-ui/app/global-error.tsx b/keep-ui/app/global-error.tsx
index 9388e06e02..1aee749cc9 100644
--- a/keep-ui/app/global-error.tsx
+++ b/keep-ui/app/global-error.tsx
@@ -1,26 +1,16 @@
"use client";
-import * as Sentry from "@sentry/nextjs";
-import NextError from "next/error";
-import { useEffect } from "react";
+import { ErrorComponent } from "@/shared/ui";
export default function GlobalError({
error,
}: {
error: Error & { digest?: string };
}) {
- useEffect(() => {
- Sentry.captureException(error);
- }, [error]);
-
return (
- {/* `NextError` is the default Next.js error page component. Its type
- definition requires a `statusCode` prop. However, since the App Router
- does not expose status codes for errors, we simply pass 0 to render a
- generic error message. */}
-
+
);
diff --git a/keep-ui/components/navbar/UserInfo.tsx b/keep-ui/components/navbar/UserInfo.tsx
index ca919e9e7f..c8cee55289 100644
--- a/keep-ui/components/navbar/UserInfo.tsx
+++ b/keep-ui/components/navbar/UserInfo.tsx
@@ -6,10 +6,9 @@ import { Session } from "next-auth";
import { useConfig } from "utils/hooks/useConfig";
import { AuthType } from "@/utils/authenticationType";
import Link from "next/link";
-import { AiOutlineRight } from "react-icons/ai";
import { VscDebugDisconnect } from "react-icons/vsc";
import { useFloating } from "@floating-ui/react";
-import { Icon, Subtitle } from "@tremor/react";
+import { Subtitle } from "@tremor/react";
import UserAvatar from "./UserAvatar";
import * as Frigade from "@frigade/react";
import { useState } from "react";
@@ -27,9 +26,6 @@ type UserDropdownProps = {
};
const UserDropdown = ({ session }: UserDropdownProps) => {
- const { userRole, user } = session;
- const { name, image, email } = user;
-
const { data: configData } = useConfig();
const signOut = useSignOut();
const { refs, floatingStyles } = useFloating({
@@ -37,6 +33,13 @@ const UserDropdown = ({ session }: UserDropdownProps) => {
strategy: "fixed",
});
+ if (!session || !session.user) {
+ return null;
+ }
+
+ const { userRole, user } = session;
+ const { name, image, email } = user;
+
const isNoAuth = configData?.AUTH_TYPE === AuthType.NOAUTH;
return (
- {isMounted && !config?.FRIGADE_DISABLED && flow?.isCompleted === false && (
- -
- setIsOnboardingOpen(true)}
- />
- setIsOnboardingOpen(false)}
- variables={{
- name: session?.user.name ?? session?.user.email,
- }}
- />
-
- )}
+ {isMounted &&
+ !config?.FRIGADE_DISABLED &&
+ flow?.isCompleted === false && (
+ -
+ setIsOnboardingOpen(true)}
+ />
+ setIsOnboardingOpen(false)}
+ variables={{
+ name: session?.user?.name ?? session?.user?.email,
+ }}
+ />
+
+ )}
-
Providers
diff --git a/keep-ui/features/incident-list/ui/incident-list-error.tsx b/keep-ui/features/incident-list/ui/incident-list-error.tsx
index b8bb6f4555..93bddc90a5 100644
--- a/keep-ui/features/incident-list/ui/incident-list-error.tsx
+++ b/keep-ui/features/incident-list/ui/incident-list-error.tsx
@@ -1,7 +1,6 @@
-import { Fragment } from "react";
-import { Button, Subtitle, Title } from "@tremor/react";
+"use client";
import NotAuthorized from "@/app/not-authorized";
-
+import { ErrorComponent } from "@/shared/ui";
interface IncidentListErrorProps {
incidentError: any;
}
@@ -13,27 +12,5 @@ export const IncidentListError = ({
return ;
}
- return (
-
-
-
-
Failed to load incidents
-
- Error: {incidentError.message}
-
-
- {incidentError.proposedResolution ||
- "Please try again. If the issue persists, contact us"}
-
-
-
-
-
- );
+ return ;
};
diff --git a/keep-ui/features/incident-list/ui/incident-list.tsx b/keep-ui/features/incident-list/ui/incident-list.tsx
index 6fcb1f73d0..5e0029afc7 100644
--- a/keep-ui/features/incident-list/ui/incident-list.tsx
+++ b/keep-ui/features/incident-list/ui/incident-list.tsx
@@ -106,20 +106,12 @@ export function IncidentList({
function renderIncidents() {
if (incidentsError) {
- return (
-
-
-
- );
+ return ;
}
if (isLoading) {
// TODO: only show this on the initial load
- return (
-
-
-
- );
+ return ;
}
if (incidents && (incidents.items.length > 0 || areFiltersApplied)) {
diff --git a/keep-ui/shared/api/ApiClient.ts b/keep-ui/shared/api/ApiClient.ts
index c878f57943..76be128875 100644
--- a/keep-ui/shared/api/ApiClient.ts
+++ b/keep-ui/shared/api/ApiClient.ts
@@ -83,7 +83,7 @@ export class ApiClient {
);
}
}
- throw new Error("An error occurred while fetching the data.");
+ throw new Error("An error occurred while fetching the data");
}
if (response.headers.get("content-length") === "0") {
diff --git a/keep-ui/shared/api/KeepApiError.ts b/keep-ui/shared/api/KeepApiError.ts
index 2d9dad937d..20796b9379 100644
--- a/keep-ui/shared/api/KeepApiError.ts
+++ b/keep-ui/shared/api/KeepApiError.ts
@@ -1,5 +1,4 @@
-// Custom Error Class
-
+// Custom Error
export class KeepApiError extends Error {
url: string;
proposedResolution: string;
@@ -37,3 +36,17 @@ export class KeepApiReadOnlyError extends KeepApiError {
this.name = "KeepReadOnlyError";
}
}
+
+export class KeepApiHealthError extends KeepApiError {
+ constructor(message: string = "API server is not available") {
+ const proposedResolution =
+ "Check if the Keep backend is running and API_URL is correct.";
+ super(message, "", proposedResolution, {}, 503);
+ this.name = "KeepApiHealthError";
+ this.message = message;
+ }
+
+ toString() {
+ return `${this.name}: ${this.message} - ${this.proposedResolution}`;
+ }
+}
diff --git a/keep-ui/shared/lib/hooks/useHealth.ts b/keep-ui/shared/lib/hooks/useHealth.ts
new file mode 100644
index 0000000000..bd5d183d59
--- /dev/null
+++ b/keep-ui/shared/lib/hooks/useHealth.ts
@@ -0,0 +1,46 @@
+import { useApi } from "@/shared/lib/hooks/useApi";
+import { useCallback, useEffect, useMemo, useState } from "react";
+
+type UseHealthResult = {
+ isHealthy: boolean;
+ lastChecked: number;
+ checkHealth: () => Promise;
+};
+
+const CACHE_DURATION = 30000;
+
+export function useHealth(): UseHealthResult {
+ const api = useApi();
+ const [isHealthy, setIsHealthy] = useState(true);
+ const [lastChecked, setLastChecked] = useState(0);
+
+ const checkHealth = useCallback(async () => {
+ // Skip if checked recently
+ if (Date.now() - lastChecked < CACHE_DURATION) {
+ return;
+ }
+
+ try {
+ await api.request("/healthcheck", {
+ method: "GET",
+ // Short timeout to avoid blocking
+ signal: AbortSignal.timeout(2000),
+ });
+ setIsHealthy(true);
+ } catch (error) {
+ setIsHealthy(false);
+ }
+ setLastChecked(Date.now());
+ }, [api]);
+
+ useEffect(() => {
+ if (!lastChecked) {
+ checkHealth();
+ }
+ }, [checkHealth, lastChecked]);
+
+ return useMemo(
+ () => ({ isHealthy, lastChecked, checkHealth }),
+ [isHealthy, lastChecked, checkHealth]
+ );
+}
diff --git a/keep-ui/shared/ui/ErrorComponent/ErrorComponent.tsx b/keep-ui/shared/ui/ErrorComponent/ErrorComponent.tsx
new file mode 100644
index 0000000000..92d619d64d
--- /dev/null
+++ b/keep-ui/shared/ui/ErrorComponent/ErrorComponent.tsx
@@ -0,0 +1,81 @@
+// The error.js file convention allows you to gracefully handle unexpected runtime errors.
+// The way it does this is by automatically wrap a route segment and its nested children in a React Error Boundary.
+// https://nextjs.org/docs/app/api-reference/file-conventions/error
+// https://nextjs.org/docs/app/building-your-application/routing/error-handling#how-errorjs-works
+
+"use client";
+import { useEffect } from "react";
+import { Title, Subtitle } from "@tremor/react";
+import { Button, Text } from "@tremor/react";
+import { KeepApiError } from "@/shared/api";
+import * as Sentry from "@sentry/nextjs";
+import { useSignOut } from "@/shared/lib/hooks/useSignOut";
+import { KeepApiHealthError } from "@/shared/api/KeepApiError";
+import { useHealth } from "@/shared/lib/hooks/useHealth";
+import { KeepLogoError } from "@/shared/ui/KeepLogoError";
+
+export function ErrorComponent({
+ error: originalError,
+ reset,
+}: {
+ error: Error | KeepApiError;
+ reset?: () => void;
+}) {
+ const signOut = useSignOut();
+ const { isHealthy } = useHealth();
+
+ useEffect(() => {
+ Sentry.captureException(originalError);
+ }, [originalError]);
+
+ const error = isHealthy ? originalError : new KeepApiHealthError();
+
+ return (
+
+
+
+
{error.message || "An error occurred"}
+ {error instanceof KeepApiError && error.proposedResolution && (
+ {error.proposedResolution}
+ )}
+
+
+ {error instanceof KeepApiError && (
+ <>
+ {error.statusCode && Status Code: {error.statusCode}
}
+ {error.message && Message: {error.message}
}
+ {error.url && URL: {error.url}
}
+ >
+ )}
+
+
+ {error instanceof KeepApiError && error.statusCode === 401 ? (
+
+ ) : (
+
+ )}{" "}
+
+
+
+ );
+}
diff --git a/keep-ui/shared/ui/ErrorComponent/index.ts b/keep-ui/shared/ui/ErrorComponent/index.ts
new file mode 100644
index 0000000000..7939a78b60
--- /dev/null
+++ b/keep-ui/shared/ui/ErrorComponent/index.ts
@@ -0,0 +1 @@
+export { ErrorComponent } from "./ErrorComponent";
diff --git a/keep-ui/shared/ui/KeepLogoError/KeepLogoError.tsx b/keep-ui/shared/ui/KeepLogoError/KeepLogoError.tsx
new file mode 100644
index 0000000000..0329c92e61
--- /dev/null
+++ b/keep-ui/shared/ui/KeepLogoError/KeepLogoError.tsx
@@ -0,0 +1,129 @@
+import React from "react";
+import Image from "next/image";
+import "./logo-error.css";
+
+export interface KeepLogoErrorProps {
+ width?: number;
+ height?: number;
+}
+
+export const KeepLogoError = ({
+ width = 200,
+ height = 200,
+}: KeepLogoErrorProps) => {
+ return (
+
+
+
+
+
+ );
+};
diff --git a/keep-ui/shared/ui/KeepLogoError/index.ts b/keep-ui/shared/ui/KeepLogoError/index.ts
new file mode 100644
index 0000000000..a99f7f054c
--- /dev/null
+++ b/keep-ui/shared/ui/KeepLogoError/index.ts
@@ -0,0 +1 @@
+export { KeepLogoError } from "./KeepLogoError";
diff --git a/keep-ui/shared/ui/KeepLogoError/keep_big.svg b/keep-ui/shared/ui/KeepLogoError/keep_big.svg
new file mode 100644
index 0000000000..a416df31c4
--- /dev/null
+++ b/keep-ui/shared/ui/KeepLogoError/keep_big.svg
@@ -0,0 +1,47 @@
+
+
+
diff --git a/keep-ui/shared/ui/KeepLogoError/logo-error.css b/keep-ui/shared/ui/KeepLogoError/logo-error.css
new file mode 100644
index 0000000000..d89dc4fab9
--- /dev/null
+++ b/keep-ui/shared/ui/KeepLogoError/logo-error.css
@@ -0,0 +1,57 @@
+.wrapper {
+ position: relative;
+ width: 16rem;
+ height: 16rem;
+}
+
+.logo-container {
+ position: absolute;
+ inset: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ filter: url(#tvNoise);
+ animation:
+ tvShift 0.1s infinite,
+ majorShift 4s infinite;
+}
+
+@keyframes tvShift {
+ 0% {
+ transform: translate(0, 0);
+ }
+ 25% {
+ transform: translate(1px, -1px);
+ }
+ 50% {
+ transform: translate(-1px, 1px);
+ }
+ 75% {
+ transform: translate(1px, 1px);
+ }
+ 100% {
+ transform: translate(0, 0);
+ }
+}
+
+@keyframes majorShift {
+ 0%,
+ 95% {
+ transform: translate(0, 0);
+ }
+ 95.2% {
+ transform: translate(15px, -8px) skew(-12deg) scale(1.1);
+ }
+ 95.7% {
+ transform: translate(-10px, -10px) skew(15deg) scale(0.95);
+ }
+ 96.2% {
+ transform: translate(8px, 12px) skew(-5deg) scale(1.05);
+ }
+ 96.7% {
+ transform: translate(-12px, 5px) skew(8deg) scale(0.9);
+ }
+ 97.2% {
+ transform: translate(0, 0);
+ }
+}
diff --git a/keep-ui/shared/ui/index.ts b/keep-ui/shared/ui/index.ts
index 2abae10ae2..d1817f1361 100644
--- a/keep-ui/shared/ui/index.ts
+++ b/keep-ui/shared/ui/index.ts
@@ -13,6 +13,7 @@ export { SeverityBorderIcon } from "./SeverityBorderIcon";
export { TableSeverityCell } from "./TableSeverityCell";
export { Select } from "./Select";
export { VerticalRoundedList } from "./VerticalRoundedList";
+export { ErrorComponent } from "./ErrorComponent";
export { getCommonPinningStylesAndClassNames } from "./utils/table-utils";
export { ThemeScript, WatchUpdateTheme, ThemeControl } from "./theme";
diff --git a/keep-ui/shared/ui/utils/showErrorToast.tsx b/keep-ui/shared/ui/utils/showErrorToast.tsx
index 32fb0c2733..2f487a485d 100644
--- a/keep-ui/shared/ui/utils/showErrorToast.tsx
+++ b/keep-ui/shared/ui/utils/showErrorToast.tsx
@@ -23,7 +23,10 @@ export function showErrorToast(
options
);
} else if (error instanceof KeepApiError) {
- toast.error(customMessage || error.message, options);
+ toast.error(
+ customMessage || `${error.message}. ${error.proposedResolution}`,
+ options
+ );
} else {
toast.error(
customMessage ||