Skip to content

Commit

Permalink
feature: add screen to view mirror activity
Browse files Browse the repository at this point in the history
  • Loading branch information
Pankaj Bhageria authored and Pankaj Bhageria committed Nov 8, 2023
1 parent fb90f2a commit 79a1938
Show file tree
Hide file tree
Showing 6 changed files with 308 additions and 32 deletions.
107 changes: 107 additions & 0 deletions ui/app/mirrors/edit/[mirrorId]/aggregatedCountsByInterval.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
type timestampType ={
timestamp: string;
count: number;
}

function aggregateCountsByInterval(timestamps: timestampType[], interval:string) {
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 :{ [key: string]: number } = {};

// 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: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:Date, format:string) {
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:number) {
return number < 10 ? `0${number}` : `${number}`;
}

export default aggregateCountsByInterval
91 changes: 66 additions & 25 deletions ui/app/mirrors/edit/[mirrorId]/cdcDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,76 @@
'use client'
import { Label } from '@/lib/Label';
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'
import moment from 'moment';

type CDCDetailsProps = {
config: FlowConnectionConfigs | undefined;

type SyncStatusRow = {
batchId: number;
startTime: Date;
endTime: Date | null;
numRows: number;
};

export default function CDCDetails({ config }: CDCDetailsProps) {
if (!config) {
return <div className='text-red-500'>No configuration provided</div>;
}
type props = {
syncs: SyncStatusRow[];
mirrorConfig: FlowConnectionConfigs | undefined;
};
function CdcDetails({ syncs,mirrorConfig }:props) {

let lastSyncedAt = moment(syncs[0]?.startTime).fromNow();
let rowsSynced = syncs.reduce((acc, sync) => acc + sync.numRows, 0)

return (
<div className='p-4 rounded-md'>
<h2 className='text-xl font-semibold mb-4'>CDC Details</h2>
<div className='overflow-x-auto'>
<table className='min-w-full divide-y divide-gray-300'>
<tbody>
<tr>
<td className='px-4 py-2 font-medium'>Source</td>
<td className='px-4 py-2'>{config.source?.name || '-'}</td>
</tr>
<tr>
<td className='px-4 py-2 font-medium'>Destination</td>
<td className='px-4 py-2'>{config.destination?.name || '-'}</td>
</tr>
<tr>
<td className='px-4 py-2 font-medium'>Flow Job Name</td>
<td className='px-4 py-2'>{config.flowJobName}</td>
</tr>
</tbody>
</table>
<>
<div className='mt-10'>
<div className="flex flex-row">
<div className="basis-1/4 md:basis-1/3">
<div><Label variant="subheadline" colorName='lowContrast'>Status</Label></div>
<div><Label variant="body">
<Badge variant='positive' key={1}>
<Icon name='play_circle' />
Active
</Badge>
</Label>
</div>
</div>
<div className="basis-1/4 md:basis-1/3">
<div><Label variant="subheadline" colorName='lowContrast'>Mirror Type</Label></div>
<div><Label variant="body">CDC</Label></div>
</div>
<div className="basis-1/4 md:basis-1/3">
<div><Label variant="subheadline" colorName='lowContrast'>Source</Label></div>
<div><Action href={"/peers/"+mirrorConfig?.source?.name}>{mirrorConfig?.source?.name}</Action></div>
</div>
<div className="basis-1/4 md:basis-1/3">
<div><Label variant="subheadline" colorName='lowContrast'>Destination</Label></div>
<div><Action href={"/peers/"+ mirrorConfig?.destination?.name}>{mirrorConfig?.destination?.name}</Action></div>
</div>
</div>
<div className="flex flex-row mt-10">
<div className="basis-1/4">
<div><Label variant="subheadline" colorName='lowContrast'>Last Sync</Label></div>
<div><Label variant="body">{lastSyncedAt}</Label></div>
</div>
<div className="basis-1/4">
<div><Label variant="subheadline" colorName='lowContrast'>Rows synced</Label></div>
<div><Label variant="body">{numberWithCommas(rowsSynced)}</Label></div>
</div>
</div>
</div>
<div className='mt-10'>
<CdcGraph syncs={syncs}/>
</div>
</>
);
}

function numberWithCommas(x:Number) {
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
}

export default CdcDetails
101 changes: 101 additions & 0 deletions ui/app/mirrors/edit/[mirrorId]/cdcGraph.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { Label } from '@/lib/Label';
import { useState, useEffect } from 'react';
import moment from 'moment';

type SyncStatusRow = {
batchId: number;
startTime: Date;
endTime: Date | null;
numRows: number;
};

import aggregateCountsByInterval from './aggregatedCountsByInterval'

const aggregateTypeMap = {
"15min":" 15 mins",
"hour": "Hour",
"day": "Day",
"month": "Month"
}

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 <div>
<div className='float-right'>
{["15min","hour","day","month"].map((type)=>{
return <FilterButton key={type} aggregateType={type} selectedAggregateType={aggregateType} setAggregateType={setAggregateType}/>
})}
</div>
<div><Label variant="body">Sync history</Label></div>

<div className='flex space-x-2 justify-left ml-2'>
{counts.map((count,i)=><GraphBar key={i} label={formatGraphLabel(new Date(count[0]),aggregateType)} count={count[1]}/>)}
</div>
</div>
}

type filterButtonProps = {
aggregateType:String;
selectedAggregateType:String;
setAggregateType:Function;
};
function FilterButton({aggregateType,selectedAggregateType,setAggregateType}:filterButtonProps){
return <button
className={aggregateType === selectedAggregateType ? "bg-gray-200 px-1 mx-1 rounded-md":"px-1 mx-1"}
onClick={()=>setAggregateType(aggregateType)}>
{aggregateTypeMap[aggregateType]}
</button>
}

type GraphBarProps = {
count: number | undefined;
label: string
};


function formatGraphLabel(date:Date, aggregateType:String) {
switch (aggregateType) {
case "15min":
return moment(date).format('MMM Do HH:mm');
case "hour":
return moment(date).format('MMM Do HH:mm');
case "day":
return moment(date).format('MMM Do');
case "month":
return moment(date).format('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 <div className={"group"}>
<div className={classNames}>
<div className="group-hover:opacity-100 transition-opacity bg-gray-800 px-1 text-sm text-gray-100 rounded-md absolute left-1/2
-translate-x-1/2 translate-y-full opacity-0 m-4 mx-auto w-28 z-10 text-center">
<div>{label}</div>
<div>{count}</div>
</div>
</div>
</div>
}


export default CdcGraph;
33 changes: 27 additions & 6 deletions ui/app/mirrors/edit/[mirrorId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ import { LayoutMain } from '@/lib/Layout';
import { GetFlowHttpAddressFromEnv } from '@/rpc/http';
import { redirect } from 'next/navigation';
import { Suspense } from 'react';
import { CDCMirror } from './cdc';
import CdcDetails from './cdcDetails';
import SyncStatus from './syncStatus';
import prisma from '@/app/utils/prisma';


export const dynamic = 'force-dynamic';

Expand Down Expand Up @@ -44,17 +46,36 @@ 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,
}));

return (
<LayoutMain alignSelf='flex-start' justifySelf='flex-start' width='full'>
<Header variant='title2'>{mirrorId}</Header>
<Suspense fallback={<Loading />}>
{mirrorStatus.cdcStatus && (
<CDCMirror
cdc={mirrorStatus.cdcStatus}
syncStatusChild={syncStatusChild}
/>
<CdcDetails syncs={rows} mirrorConfig={mirrorStatus.cdcStatus.config} />
)}
<div className='mt-10'>
{syncStatusChild}
</div>
</Suspense>
</LayoutMain>
);
}
}
5 changes: 4 additions & 1 deletion ui/app/mirrors/edit/[mirrorId]/syncStatus.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import prisma from '@/app/utils/prisma';
import { SyncStatusTable } from './syncStatusTable';
import NewCdcDetails from './newCdcDetails';

type SyncStatusProps = {
flowJobName: string | undefined;
Expand Down Expand Up @@ -29,5 +30,7 @@ export default async function SyncStatus({ flowJobName }: SyncStatusProps) {
numRows: sync.rows_in_batch,
}));

return <SyncStatusTable rows={rows} />;
return <div>
<SyncStatusTable rows={rows} />
</div>;
}
3 changes: 3 additions & 0 deletions ui/app/mirrors/status/timeline.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

//React component to show a timeline of sync events. Each bar on the time line will
//represent the number of

0 comments on commit 79a1938

Please sign in to comment.