diff --git a/ui/app/mirrors/edit/[mirrorId]/aggregatedCountsByInterval.js b/ui/app/mirrors/edit/[mirrorId]/aggregatedCountsByInterval.js
new file mode 100644
index 0000000000..2895c917b1
--- /dev/null
+++ b/ui/app/mirrors/edit/[mirrorId]/aggregatedCountsByInterval.js
@@ -0,0 +1,102 @@
+function aggregateCountsByInterval(timestamps, interval) {
+ let timeUnit;
+ switch (interval) {
+ case 'hour':
+ timeUnit = 'YYYY-MM-DD HH:00:00';
+ break;
+ case '15min':
+ timeUnit = 'YYYY-MM-DD HH:mm';
+ break;
+ case 'month':
+ timeUnit = 'YYYY-MM';
+ break;
+ case 'day':
+ timeUnit = 'YYYY-MM-DD';
+ break;
+ default:
+ throw new Error('Invalid interval provided');
+ }
+
+ // Create an object to store the aggregated counts
+ const aggregatedCounts = {};
+
+ // Iterate through the timestamps and populate the aggregatedCounts object
+ for (let { timestamp, count } of timestamps) {
+ timestamp = roundUpToNearest15Minutes(timestamp);
+ const date = new Date(timestamp);
+ const formattedTimestamp = formatTimestamp(date, timeUnit);
+
+ if (!aggregatedCounts[formattedTimestamp]) {
+ aggregatedCounts[formattedTimestamp] = 0;
+ }
+
+ aggregatedCounts[formattedTimestamp] += count;
+ }
+
+
+
+ // Create an array of intervals between the start and end timestamps
+ const intervals = [];
+
+ let currentTimestamp = new Date();
+
+ if(interval === "15min"){
+ currentTimestamp = roundUpToNearest15Minutes(currentTimestamp);
+ }
+
+ while (intervals.length < 30) {
+ intervals.push(formatTimestamp(currentTimestamp, timeUnit));
+ if (interval === 'hour') {
+ currentTimestamp.setHours(currentTimestamp.getHours() - 1);
+ } else if (interval === '15min') {
+ currentTimestamp.setMinutes(currentTimestamp.getMinutes() - 15);
+ } else if (interval === 'month') {
+ currentTimestamp.setMonth(currentTimestamp.getMonth() - 1);
+ } else if(interval === 'day'){
+ currentTimestamp.setDate(currentTimestamp.getDate() - 1);
+ }
+ }
+
+ // Populate the result array with intervals and counts
+ const resultArray = intervals.map((interval) => [interval, aggregatedCounts[interval] || 0]);
+ return resultArray;
+ }
+
+ function roundUpToNearest15Minutes(date) {
+ const minutes = date.getMinutes();
+ const remainder = minutes % 15;
+
+ if (remainder > 0) {
+ // Round up to the nearest 15 minutes
+ date.setMinutes(minutes + (15 - remainder));
+ }
+
+ // Reset seconds and milliseconds to zero to maintain the same time
+ date.setSeconds(0);
+ date.setMilliseconds(0);
+
+ return date;
+ }
+
+ // Helper function to format a timestamp
+ function formatTimestamp(date, format) {
+ const year = date.getFullYear();
+ const month = padZero(date.getMonth() + 1); // Months are zero-based
+ const day = padZero(date.getDate());
+ const hour = padZero(date.getHours());
+ const minutes = padZero(date.getMinutes());
+
+ return format
+ .replace('YYYY', year)
+ .replace('MM', month)
+ .replace('DD', day)
+ .replace('HH', hour)
+ .replace('mm', minutes);
+ }
+
+ // Helper function to pad single digits with leading zeros
+ function padZero(number) {
+ return number < 10 ? `0${number}` : `${number}`;
+ }
+
+export default aggregateCountsByInterval
\ No newline at end of file
diff --git a/ui/app/mirrors/edit/[mirrorId]/cdcGraph.tsx b/ui/app/mirrors/edit/[mirrorId]/cdcGraph.tsx
new file mode 100644
index 0000000000..701076b669
--- /dev/null
+++ b/ui/app/mirrors/edit/[mirrorId]/cdcGraph.tsx
@@ -0,0 +1,82 @@
+import { Label } from '@/lib/Label';
+import { useState, useEffect } from 'react';
+import { format } from 'date-fns'
+
+type SyncStatusRow = {
+ batchId: number;
+ startTime: Date;
+ endTime: Date | null;
+ numRows: number;
+};
+
+import aggregateCountsByInterval from './aggregatedCountsByInterval'
+
+ function CdcGraph({syncs}:{syncs:SyncStatusRow[]}) {
+
+ let [aggregateType,setAggregateType] = useState('hour');
+ let [counts,setCounts] = useState([]);
+
+
+ let rows = syncs.map((sync) => ({
+ timestamp: sync.startTime,
+ count: sync.numRows,
+ }))
+
+ useEffect(()=>{
+ let counts = aggregateCountsByInterval(rows,aggregateType, undefined, new Date());
+ counts = counts.slice(0,29)
+ counts = counts.reverse();
+ setCounts(counts)
+
+ },[aggregateType, rows])
+
+ return
+
+ setAggregateType('15min')}>15 mins
+ setAggregateType('hour')}>Hour
+ setAggregateType('day')}>Day
+ setAggregateType('month')}>Month
+
+
Sync history
+
+
+ {counts.map((count,i)=>)}
+
+
+ }
+
+ type GraphBarProps = {
+ count: number | undefined;
+ label: string
+ };
+
+
+ function formatGraphLabel(date:Date, aggregateType:String) {
+ switch (aggregateType) {
+ case "15min":
+ return format(date, 'MMM dd HH:mm');
+ case "hour":
+ return format(date, 'MMM dd HH:mm');
+ case "day":
+ return format(date, 'MMM dd');
+ case "month":
+ return format(date, 'MMM yy');
+ }
+ }
+
+ function GraphBar({label,count}:GraphBarProps){
+ let color = count && count >0 ? 'bg-green-500' : 'bg-gray-500';
+ let classNames = `relative w-10 h-24 rounded ${color}`;
+ return
+ }
+
+
+ export default CdcGraph;
diff --git a/ui/app/mirrors/edit/[mirrorId]/newCdcDetails.tsx b/ui/app/mirrors/edit/[mirrorId]/newCdcDetails.tsx
new file mode 100644
index 0000000000..77881af09e
--- /dev/null
+++ b/ui/app/mirrors/edit/[mirrorId]/newCdcDetails.tsx
@@ -0,0 +1,79 @@
+'use client'
+import { Label } from '@/lib/Label';
+import {formatDistance } from 'date-fns'
+import { Badge } from '@/lib/Badge';
+import { Icon } from '@/lib/Icon';
+import {Action} from '@/lib/Action';
+import { FlowConnectionConfigs } from '@/grpc_generated/flow';
+import CdcGraph from './cdcGraph'
+
+
+
+type SyncStatusRow = {
+ batchId: number;
+ startTime: Date;
+ endTime: Date | null;
+ numRows: number;
+};
+
+type props = {
+ syncs: SyncStatusRow[];
+ mirrorConfig: FlowConnectionConfigs | undefined;
+ };
+function CdcDetails({ syncs,mirrorConfig }:props) {
+
+ let lastSyncedAt = formatDistance(syncs[0]?.startTime, new Date(), { addSuffix: true })
+
+ let rowsSynced = syncs.reduce((acc, sync) => acc + sync.numRows, 0)
+
+ return (
+ <>
+
+
+
+
Status
+
+
+
+ Active
+
+
+
+
+
+
+
Source
+
{mirrorConfig?.source?.name}
+
+
+
Destination
+
{mirrorConfig?.destination?.name}
+
+
+
+
+
Last Sync
+
{lastSyncedAt}
+
+
+
Rows synced
+
{numberWithCommas(rowsSynced)}
+
+
+
+
+
+
+ >
+ );
+}
+
+export default CdcDetails
+
+
+ function numberWithCommas(x:Number) {
+ return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
+}
\ No newline at end of file
diff --git a/ui/app/mirrors/edit/[mirrorId]/page.tsx b/ui/app/mirrors/edit/[mirrorId]/page.tsx
index e2c5e6596a..9dfca9d07a 100644
--- a/ui/app/mirrors/edit/[mirrorId]/page.tsx
+++ b/ui/app/mirrors/edit/[mirrorId]/page.tsx
@@ -6,6 +6,9 @@ import { redirect } from 'next/navigation';
import { Suspense } from 'react';
import { CDCMirror } from './cdc';
import SyncStatus from './syncStatus';
+import NewCdcDetails from './newCdcDetails';
+import prisma from '@/app/utils/prisma';
+
export const dynamic = 'force-dynamic';
@@ -44,17 +47,61 @@ export default async function EditMirror({
redirect(`/mirrors/status/qrep/${mirrorId}`);
}
+ let syncs = await prisma.cdc_batches.findMany({
+ where: {
+ flow_name: mirrorId,
+ start_time: {
+ not: undefined,
+ },
+ },
+ orderBy: {
+ start_time: 'desc',
+ },
+ })
+
+ const rows = syncs.map((sync) => ({
+ batchId: sync.id,
+ startTime: sync.start_time,
+ endTime: sync.end_time,
+ numRows: sync.rows_in_batch,
+ }));
+
+ console.log("*********_________-------",mirrorStatus.cdcStatus.config);
+
return (
}>
{mirrorStatus.cdcStatus && (
-
+ //
+
+
)}
+
+ {syncStatusChild}
+
);
}
+
+
+// export const getServerSideProps = (async (context) => {
+// console.log("*****************in getServerSideProps", context)
+// let syncs = await prisma.cdc_batches.findMany({
+// where: {
+// flow_name: flowJobName,
+// start_time: {
+// not: undefined,
+// },
+// },
+// orderBy: {
+// start_time: 'desc',
+// },
+// })
+
+// return { props: { syncs } }
+// })
diff --git a/ui/app/mirrors/edit/[mirrorId]/syncStatus.tsx b/ui/app/mirrors/edit/[mirrorId]/syncStatus.tsx
index 80cb35701c..562e9f9e1f 100644
--- a/ui/app/mirrors/edit/[mirrorId]/syncStatus.tsx
+++ b/ui/app/mirrors/edit/[mirrorId]/syncStatus.tsx
@@ -1,5 +1,6 @@
import prisma from '@/app/utils/prisma';
import { SyncStatusTable } from './syncStatusTable';
+import NewCdcDetails from './newCdcDetails';
type SyncStatusProps = {
flowJobName: string | undefined;
@@ -29,5 +30,7 @@ export default async function SyncStatus({ flowJobName }: SyncStatusProps) {
numRows: sync.rows_in_batch,
}));
- return ;
+ return
+
+
;
}
diff --git a/ui/app/mirrors/edit/[mirrorId]/syncStatusTable.tsx b/ui/app/mirrors/edit/[mirrorId]/syncStatusTable.tsx
index 752b24e8c3..741cfeb028 100644
--- a/ui/app/mirrors/edit/[mirrorId]/syncStatusTable.tsx
+++ b/ui/app/mirrors/edit/[mirrorId]/syncStatusTable.tsx
@@ -70,7 +70,7 @@ export const SyncStatusTable = ({ rows }: SyncStatusTableProps) => {
return (
CDC Syncs}
+ title={Batches }
toolbar={{
left: (
<>
diff --git a/ui/app/mirrors/status/timeline.tsx b/ui/app/mirrors/status/timeline.tsx
new file mode 100644
index 0000000000..6941617820
--- /dev/null
+++ b/ui/app/mirrors/status/timeline.tsx
@@ -0,0 +1,3 @@
+
+//React component to show a timeline of sync events. Each bar on the time line will
+//represent the number of
\ No newline at end of file