diff --git a/ui/app/api/mirrors/alerts/route.ts b/ui/app/api/mirrors/alerts/route.ts
new file mode 100644
index 0000000000..ecb9891cbd
--- /dev/null
+++ b/ui/app/api/mirrors/alerts/route.ts
@@ -0,0 +1,21 @@
+import prisma from '@/app/utils/prisma';
+
+export const dynamic = 'force-dynamic';
+
+export async function POST(request: Request) {
+ const { flowName } = await request.json();
+ const errCount = await prisma.flow_errors.count({
+ where: {
+ flow_name: flowName,
+ error_type: 'error',
+ ack: false,
+ },
+ });
+ let mirrorStatus: 'healthy' | 'failed';
+ if (errCount > 0) {
+ mirrorStatus = 'failed';
+ } else {
+ mirrorStatus = 'healthy';
+ }
+ return new Response(JSON.stringify(mirrorStatus));
+}
diff --git a/ui/app/dto/MirrorsDTO.ts b/ui/app/dto/MirrorsDTO.ts
index 4a76200fd4..977f7f353c 100644
--- a/ui/app/dto/MirrorsDTO.ts
+++ b/ui/app/dto/MirrorsDTO.ts
@@ -28,3 +28,12 @@ export type SyncStatusRow = {
endTime: Date | null;
numRows: number;
};
+
+export type AlertErr = {
+ id: bigint;
+ flow_name: string;
+ error_message: string;
+ error_type: string;
+ error_timestamp: Date;
+ ack: boolean;
+};
diff --git a/ui/app/mirrors/errors/[mirrorName]/page.tsx b/ui/app/mirrors/errors/[mirrorName]/page.tsx
new file mode 100644
index 0000000000..75e8c91d2b
--- /dev/null
+++ b/ui/app/mirrors/errors/[mirrorName]/page.tsx
@@ -0,0 +1,75 @@
+import { AlertErr } from '@/app/dto/MirrorsDTO';
+import prisma from '@/app/utils/prisma';
+import TimeLabel from '@/components/TimeComponent';
+import { Label } from '@/lib/Label';
+import { Table, TableCell, TableRow } from '@/lib/Table';
+
+type MirrorErrorProps = {
+ params: { mirrorName: string };
+};
+
+const MirrorError = async ({ params: { mirrorName } }: MirrorErrorProps) => {
+ const mirrorErrors: AlertErr[] = await prisma.flow_errors.findMany({
+ where: {
+ flow_name: mirrorName,
+ error_type: 'error',
+ },
+ distinct: ['error_message'],
+ });
+
+ return (
+
+
+
+
+
+
+
+
+ Type
+ Message
+
+
+
+
+ }
+ >
+ {mirrorErrors.map((mirrorError) => (
+
+
+ {mirrorError.error_type.toUpperCase()}
+
+
+ {mirrorError.error_message}
+
+
+
+
+
+ ))}
+
+
+
+
+ );
+};
+
+export default MirrorError;
diff --git a/ui/app/mirrors/mirror-status.tsx b/ui/app/mirrors/mirror-status.tsx
new file mode 100644
index 0000000000..2a68b7b3d2
--- /dev/null
+++ b/ui/app/mirrors/mirror-status.tsx
@@ -0,0 +1,83 @@
+'use client';
+
+import { Button } from '@/lib/Button';
+import { Icon } from '@/lib/Icon';
+import { Label } from '@/lib/Label';
+import { ProgressCircle } from '@/lib/ProgressCircle';
+import Link from 'next/link';
+import { useRouter } from 'next/navigation';
+import { useEffect, useState } from 'react';
+export const ErrorModal = ({ flowName }: { flowName: string }) => {
+ const router = useRouter();
+ return (
+
+
+
+ );
+};
+
+export const MirrorError = ({ flowName }: { flowName: string }) => {
+ const [flowStatus, setFlowStatus] = useState();
+ const [isLoading, setIsLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ const fetchData = async () => {
+ setIsLoading(true);
+ try {
+ const response = await fetch(`/api/mirrors/alerts`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({ flowName }),
+ });
+
+ if (!response.ok) {
+ throw new Error('Network response was not ok');
+ }
+
+ const flowStatus = await response.json();
+ setFlowStatus(flowStatus);
+ } catch (err: any) {
+ setError(err.message);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ fetchData();
+ }, [flowName]);
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
+ );
+ }
+
+ if (flowStatus == 'healthy') {
+ return ;
+ }
+
+ return ;
+};
diff --git a/ui/app/mirrors/tables.tsx b/ui/app/mirrors/tables.tsx
index 743bbc28a4..6c1289befc 100644
--- a/ui/app/mirrors/tables.tsx
+++ b/ui/app/mirrors/tables.tsx
@@ -7,6 +7,7 @@ import { SearchField } from '@/lib/SearchField';
import { Table, TableCell, TableRow } from '@/lib/Table';
import Link from 'next/link';
import { useMemo, useState } from 'react';
+import { MirrorError } from './mirror-status';
export function CDCFlows({ cdcFlows }: { cdcFlows: any }) {
const [searchQuery, setSearchQuery] = useState('');
@@ -43,15 +44,26 @@ export function CDCFlows({ cdcFlows }: { cdcFlows: any }) {
}}
header={
- {['Name', 'Source', 'Destination', 'Start Time', ''].map(
- (heading, index) => (
-
-
-
- )
- )}
+ {[
+ 'Name',
+ 'Source',
+ 'Destination',
+ 'Start Time',
+ 'Status',
+ '',
+ ].map((heading, index) => (
+
+
+
+ ))}
}
>
@@ -77,6 +89,9 @@ export function CDCFlows({ cdcFlows }: { cdcFlows: any }) {
+
+
+