Skip to content

Commit 03c888c

Browse files
Pankaj BhageriaPankaj Bhageria
Pankaj Bhageria
authored and
Pankaj Bhageria
committed
feature: add screen to view mirror activity
1 parent fb90f2a commit 03c888c

File tree

6 files changed

+289
-32
lines changed

6 files changed

+289
-32
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
type timestampType ={
2+
timestamp: string;
3+
count: number;
4+
}
5+
6+
function aggregateCountsByInterval(timestamps: timestampType[], interval:string) {
7+
let timeUnit;
8+
switch (interval) {
9+
case 'hour':
10+
timeUnit = 'YYYY-MM-DD HH:00:00';
11+
break;
12+
case '15min':
13+
timeUnit = 'YYYY-MM-DD HH:mm';
14+
break;
15+
case 'month':
16+
timeUnit = 'YYYY-MM';
17+
break;
18+
case 'day':
19+
timeUnit = 'YYYY-MM-DD';
20+
break;
21+
default:
22+
throw new Error('Invalid interval provided');
23+
}
24+
25+
// Create an object to store the aggregated counts
26+
const aggregatedCounts :{ [key: string]: number } = {};
27+
28+
// Iterate through the timestamps and populate the aggregatedCounts object
29+
for (let { timestamp, count } of timestamps) {
30+
timestamp = roundUpToNearest15Minutes(timestamp);
31+
const date = new Date(timestamp);
32+
const formattedTimestamp = formatTimestamp(date, timeUnit);
33+
34+
if (!aggregatedCounts[formattedTimestamp]) {
35+
aggregatedCounts[formattedTimestamp] = 0;
36+
}
37+
38+
aggregatedCounts[formattedTimestamp] += count;
39+
}
40+
41+
42+
43+
// Create an array of intervals between the start and end timestamps
44+
const intervals = [];
45+
46+
let currentTimestamp = new Date();
47+
48+
if(interval === "15min"){
49+
currentTimestamp = roundUpToNearest15Minutes(currentTimestamp);
50+
}
51+
52+
while (intervals.length < 30) {
53+
intervals.push(formatTimestamp(currentTimestamp, timeUnit));
54+
if (interval === 'hour') {
55+
currentTimestamp.setHours(currentTimestamp.getHours() - 1);
56+
} else if (interval === '15min') {
57+
currentTimestamp.setMinutes(currentTimestamp.getMinutes() - 15);
58+
} else if (interval === 'month') {
59+
currentTimestamp.setMonth(currentTimestamp.getMonth() - 1);
60+
} else if(interval === 'day'){
61+
currentTimestamp.setDate(currentTimestamp.getDate() - 1);
62+
}
63+
}
64+
65+
// Populate the result array with intervals and counts
66+
const resultArray = intervals.map((interval) => [interval, aggregatedCounts[interval] || 0]);
67+
return resultArray;
68+
}
69+
70+
function roundUpToNearest15Minutes(date:Date) {
71+
const minutes = date.getMinutes();
72+
const remainder = minutes % 15;
73+
74+
if (remainder > 0) {
75+
// Round up to the nearest 15 minutes
76+
date.setMinutes(minutes + (15 - remainder));
77+
}
78+
79+
// Reset seconds and milliseconds to zero to maintain the same time
80+
date.setSeconds(0);
81+
date.setMilliseconds(0);
82+
83+
return date;
84+
}
85+
86+
// Helper function to format a timestamp
87+
function formatTimestamp(date:Date, format:string) {
88+
const year = date.getFullYear();
89+
const month = padZero(date.getMonth() + 1); // Months are zero-based
90+
const day = padZero(date.getDate());
91+
const hour = padZero(date.getHours());
92+
const minutes = padZero(date.getMinutes());
93+
94+
return format
95+
.replace('YYYY', year)
96+
.replace('MM', month)
97+
.replace('DD', day)
98+
.replace('HH', hour)
99+
.replace('mm', minutes);
100+
}
101+
102+
// Helper function to pad single digits with leading zeros
103+
function padZero(number:number) {
104+
return number < 10 ? `0${number}` : `${number}`;
105+
}
106+
107+
export default aggregateCountsByInterval
+66-25
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,76 @@
1+
'use client'
2+
import { Label } from '@/lib/Label';
3+
import { Badge } from '@/lib/Badge';
4+
import { Icon } from '@/lib/Icon';
5+
import {Action} from '@/lib/Action';
16
import { FlowConnectionConfigs } from '@/grpc_generated/flow';
7+
import CdcGraph from './cdcGraph'
8+
import moment from 'moment';
29

3-
type CDCDetailsProps = {
4-
config: FlowConnectionConfigs | undefined;
10+
11+
type SyncStatusRow = {
12+
batchId: number;
13+
startTime: Date;
14+
endTime: Date | null;
15+
numRows: number;
516
};
617

7-
export default function CDCDetails({ config }: CDCDetailsProps) {
8-
if (!config) {
9-
return <div className='text-red-500'>No configuration provided</div>;
10-
}
18+
type props = {
19+
syncs: SyncStatusRow[];
20+
mirrorConfig: FlowConnectionConfigs | undefined;
21+
};
22+
function CdcDetails({ syncs,mirrorConfig }:props) {
23+
24+
let lastSyncedAt = moment(syncs[0]?.startTime).fromNow();
25+
let rowsSynced = syncs.reduce((acc, sync) => acc + sync.numRows, 0)
1126

1227
return (
13-
<div className='p-4 rounded-md'>
14-
<h2 className='text-xl font-semibold mb-4'>CDC Details</h2>
15-
<div className='overflow-x-auto'>
16-
<table className='min-w-full divide-y divide-gray-300'>
17-
<tbody>
18-
<tr>
19-
<td className='px-4 py-2 font-medium'>Source</td>
20-
<td className='px-4 py-2'>{config.source?.name || '-'}</td>
21-
</tr>
22-
<tr>
23-
<td className='px-4 py-2 font-medium'>Destination</td>
24-
<td className='px-4 py-2'>{config.destination?.name || '-'}</td>
25-
</tr>
26-
<tr>
27-
<td className='px-4 py-2 font-medium'>Flow Job Name</td>
28-
<td className='px-4 py-2'>{config.flowJobName}</td>
29-
</tr>
30-
</tbody>
31-
</table>
28+
<>
29+
<div className='mt-10'>
30+
<div className="flex flex-row">
31+
<div className="basis-1/4 md:basis-1/3">
32+
<div><Label variant="subheadline" colorName='lowContrast'>Status</Label></div>
33+
<div><Label variant="body">
34+
<Badge variant='positive' key={1}>
35+
<Icon name='play_circle' />
36+
Active
37+
</Badge>
38+
</Label>
39+
</div>
40+
</div>
41+
<div className="basis-1/4 md:basis-1/3">
42+
<div><Label variant="subheadline" colorName='lowContrast'>Mirror Type</Label></div>
43+
<div><Label variant="body">CDC</Label></div>
44+
</div>
45+
<div className="basis-1/4 md:basis-1/3">
46+
<div><Label variant="subheadline" colorName='lowContrast'>Source</Label></div>
47+
<div><Action href={"/peers/"+mirrorConfig?.source?.name}>{mirrorConfig?.source?.name}</Action></div>
48+
</div>
49+
<div className="basis-1/4 md:basis-1/3">
50+
<div><Label variant="subheadline" colorName='lowContrast'>Destination</Label></div>
51+
<div><Action href={"/peers/"+ mirrorConfig?.destination?.name}>{mirrorConfig?.destination?.name}</Action></div>
52+
</div>
53+
</div>
54+
<div className="flex flex-row mt-10">
55+
<div className="basis-1/4">
56+
<div><Label variant="subheadline" colorName='lowContrast'>Last Sync</Label></div>
57+
<div><Label variant="body">{lastSyncedAt}</Label></div>
58+
</div>
59+
<div className="basis-1/4">
60+
<div><Label variant="subheadline" colorName='lowContrast'>Rows synced</Label></div>
61+
<div><Label variant="body">{numberWithCommas(rowsSynced)}</Label></div>
62+
</div>
3263
</div>
3364
</div>
65+
<div className='mt-10'>
66+
<CdcGraph syncs={syncs}/>
67+
</div>
68+
</>
3469
);
3570
}
71+
72+
function numberWithCommas(x:Number) {
73+
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
74+
}
75+
76+
export default CdcDetails
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import { Label } from '@/lib/Label';
2+
import { useState, useEffect } from 'react';
3+
import moment from 'moment';
4+
5+
type SyncStatusRow = {
6+
batchId: number;
7+
startTime: Date;
8+
endTime: Date | null;
9+
numRows: number;
10+
};
11+
12+
import aggregateCountsByInterval from './aggregatedCountsByInterval'
13+
14+
function CdcGraph({syncs}:{syncs:SyncStatusRow[]}) {
15+
16+
let [aggregateType,setAggregateType] = useState('hour');
17+
let [counts,setCounts] = useState([]);
18+
19+
20+
let rows = syncs.map((sync) => ({
21+
timestamp: sync.startTime,
22+
count: sync.numRows,
23+
}))
24+
25+
useEffect(()=>{
26+
let counts = aggregateCountsByInterval(rows,aggregateType, undefined, new Date());
27+
counts = counts.slice(0,29)
28+
counts = counts.reverse();
29+
setCounts(counts)
30+
31+
},[aggregateType, rows])
32+
33+
return <div>
34+
<div className='float-right'>
35+
<button className={aggregateType === "15min" ? "bg-gray-200 px-1 mx-1 rounded-md":"px-1 mx-1"} onClick={()=>setAggregateType('15min')}>15 mins</button>
36+
<button className={aggregateType === "hour" ? "bg-gray-200 px-1 mx-1 rounded-md":"px-1 mx-1"} onClick={()=>setAggregateType('hour')}>Hour</button>
37+
<button className={aggregateType === "day" ? "bg-gray-200 px-1 mx-1 rounded-md":"px-1 mx-1"} onClick={()=>setAggregateType('day')}>Day</button>
38+
<button className={aggregateType === "month" ? "bg-gray-200 px-1 mx-1 rounded-md":"px-1 mx-1"} onClick={()=>setAggregateType('month')}>Month</button>
39+
</div>
40+
<div><Label variant="body">Sync history</Label></div>
41+
42+
<div className='flex space-x-2 justify-left ml-2'>
43+
{counts.map((count,i)=><GraphBar key={i} label={formatGraphLabel(new Date(count[0]),aggregateType)} count={count[1]}/>)}
44+
</div>
45+
</div>
46+
}
47+
48+
type GraphBarProps = {
49+
count: number | undefined;
50+
label: string
51+
};
52+
53+
54+
function formatGraphLabel(date:Date, aggregateType:String) {
55+
switch (aggregateType) {
56+
case "15min":
57+
return moment(date).format('MMM Do HH:mm');
58+
case "hour":
59+
return moment(date).format('MMM Do HH:mm');
60+
case "day":
61+
return moment(date).format('MMM Do');
62+
case "month":
63+
return moment(date).format('MMM yy');
64+
}
65+
}
66+
67+
function GraphBar({label,count}:GraphBarProps){
68+
let color = count && count >0 ? 'bg-green-500' : 'bg-gray-500';
69+
let classNames = `relative w-10 h-24 rounded ${color}`;
70+
return <div className={"group"}>
71+
<div className={classNames}>
72+
<div className="group-hover:opacity-100 transition-opacity bg-gray-800 px-1 text-sm text-gray-100 rounded-md absolute left-1/2
73+
-translate-x-1/2 translate-y-full opacity-0 m-4 mx-auto w-28 z-10 text-center">
74+
<div>{label}</div>
75+
<div>{count}</div>
76+
</div>
77+
</div>
78+
</div>
79+
}
80+
81+
82+
export default CdcGraph;

ui/app/mirrors/edit/[mirrorId]/page.tsx

+27-6
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@ import { LayoutMain } from '@/lib/Layout';
44
import { GetFlowHttpAddressFromEnv } from '@/rpc/http';
55
import { redirect } from 'next/navigation';
66
import { Suspense } from 'react';
7-
import { CDCMirror } from './cdc';
7+
import CdcDetails from './cdcDetails';
88
import SyncStatus from './syncStatus';
9+
import prisma from '@/app/utils/prisma';
10+
911

1012
export const dynamic = 'force-dynamic';
1113

@@ -44,17 +46,36 @@ export default async function EditMirror({
4446
redirect(`/mirrors/status/qrep/${mirrorId}`);
4547
}
4648

49+
let syncs = await prisma.cdc_batches.findMany({
50+
where: {
51+
flow_name: mirrorId,
52+
start_time: {
53+
not: undefined,
54+
},
55+
},
56+
orderBy: {
57+
start_time: 'desc',
58+
},
59+
})
60+
61+
const rows = syncs.map((sync) => ({
62+
batchId: sync.id,
63+
startTime: sync.start_time,
64+
endTime: sync.end_time,
65+
numRows: sync.rows_in_batch,
66+
}));
67+
4768
return (
4869
<LayoutMain alignSelf='flex-start' justifySelf='flex-start' width='full'>
4970
<Header variant='title2'>{mirrorId}</Header>
5071
<Suspense fallback={<Loading />}>
5172
{mirrorStatus.cdcStatus && (
52-
<CDCMirror
53-
cdc={mirrorStatus.cdcStatus}
54-
syncStatusChild={syncStatusChild}
55-
/>
73+
<CdcDetails syncs={rows} mirrorConfig={mirrorStatus.cdcStatus.config} />
5674
)}
75+
<div className='mt-10'>
76+
{syncStatusChild}
77+
</div>
5778
</Suspense>
5879
</LayoutMain>
5980
);
60-
}
81+
}

ui/app/mirrors/edit/[mirrorId]/syncStatus.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import prisma from '@/app/utils/prisma';
22
import { SyncStatusTable } from './syncStatusTable';
3+
import NewCdcDetails from './newCdcDetails';
34

45
type SyncStatusProps = {
56
flowJobName: string | undefined;
@@ -29,5 +30,7 @@ export default async function SyncStatus({ flowJobName }: SyncStatusProps) {
2930
numRows: sync.rows_in_batch,
3031
}));
3132

32-
return <SyncStatusTable rows={rows} />;
33+
return <div>
34+
<SyncStatusTable rows={rows} />
35+
</div>;
3336
}

ui/app/mirrors/status/timeline.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
2+
//React component to show a timeline of sync events. Each bar on the time line will
3+
//represent the number of

0 commit comments

Comments
 (0)