diff --git a/ui/app/mirrors/edit/[mirrorId]/aggregatedCountsByInterval.js b/ui/app/mirrors/edit/[mirrorId]/aggregatedCountsByInterval.js new file mode 100644 index 0000000000..89acfd648c --- /dev/null +++ b/ui/app/mirrors/edit/[mirrorId]/aggregatedCountsByInterval.js @@ -0,0 +1,126 @@ +function aggregateCountsByInterval(timestamps, interval, startTimestamp, endTimestamp) { + //startTimestamp = roundUpToNearest15Minutes(startTimestamp); + // Define the time unit based on the provided 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; + } + + + // Get the start and end timestamps or use the earliest and latest timestamps from the data + //const firstTimestamp = new Date(timestamps[0].timestamp); + //const lastTimestamp = new Date(timestamps[timestamps.length - 1].timestamp); + //startTimestamp = startTimestamp ? new Date(startTimestamp) : firstTimestamp; + //endTimestamp = endTimestamp ? new Date(endTimestamp) : lastTimestamp; + + // Create an array of intervals between the start and end timestamps + const intervals = []; + //let currentTimestamp = new Date(startTimestamp); + 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}`; + } + +// // Example usage +// const timestamps = [ +// { timestamp: '2023-11-03T09:30:00', count: 5 }, +// { timestamp: '2023-11-03T10:15:00', count: 10 }, +// { timestamp: '2023-11-03T11:45:00', count: 7 }, +// // Add more timestamps as needed +// ]; + +// const resultByHour = aggregateCountsByInterval(timestamps, 'hour', '2023-11-03T09:00:00', '2023-11-03T11:00:00'); +// const resultBy15Min = aggregateCountsByInterval(timestamps, '15min', '2023-11-03T09:00:00', '2023-11-03T11:30:00'); +// const resultByMonth = aggregateCountsByInterval(timestamps, 'month', '2023-11-01', '2023-11-03'); + +// console.log('Counts by Hour:', resultByHour); +// console.log('Counts by 15 Minutes:', resultBy15Min); +// console.log('Counts by Month:', resultByMonth); + + +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
+
+ + + + +
+
+ +
+ {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
+
+
+
{label}
+
{count}
+
+
+
+ } + + + 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 ( + <> +
+
+
+
+
+
+
+
+
+
+
+
+
+
{mirrorConfig?.source?.name}
+
+
+
+
{mirrorConfig?.destination?.name}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+ + ); +} + +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 (
{mirrorId}
}> {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={} 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