Skip to content

Commit

Permalink
feature: add screen to view mirror activity (#627)
Browse files Browse the repository at this point in the history
  • Loading branch information
pankaj-peerdb authored Nov 10, 2023
1 parent 1dcfb09 commit de62965
Show file tree
Hide file tree
Showing 7 changed files with 367 additions and 33 deletions.
92 changes: 92 additions & 0 deletions ui/app/mirrors/edit/[mirrorId]/aggregatedCountsByInterval.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import moment from 'moment';

type timestampType = {
timestamp: Date;
count: number;
};

function aggregateCountsByInterval(
timestamps: timestampType[],
interval: string
): [string, number][] {
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) {
const date = roundUpToNearest15Minutes(timestamp);
const formattedTimestamp = moment(date).format(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(moment(currentTimestamp).format(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: [string, number][] = 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;
}

export default aggregateCountsByInterval;
3 changes: 1 addition & 2 deletions ui/app/mirrors/edit/[mirrorId]/cdc.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import { useQueryState } from 'next-usequerystate';
import Link from 'next/link';
import { useState } from 'react';
import styled from 'styled-components';
import CDCDetails from './cdcDetails';

class TableCloneSummary {
flowJobName: string;
Expand Down Expand Up @@ -243,7 +242,7 @@ export function CDCMirror({ cdc, syncStatusChild }: CDCMirrorStatusProps) {
</Trigger>
</Tabs.List>
<Tabs.Content className='p-5 rounded-b-md' value='tab1'>
<CDCDetails config={cdc.config} />
{/* <CDCDetails config={cdc.config} /> */}
</Tabs.Content>
<Tabs.Content className='p-5 rounded-b-md' value='tab2'>
{syncStatusChild}
Expand Down
134 changes: 108 additions & 26 deletions ui/app/mirrors/edit/[mirrorId]/cdcDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,117 @@
'use client';
import { FlowConnectionConfigs } from '@/grpc_generated/flow';
import { Badge } from '@/lib/Badge';
import { Icon } from '@/lib/Icon';
import { Label } from '@/lib/Label';
import moment from 'moment';
import CdcGraph from './cdcGraph';

type CDCDetailsProps = {
config: FlowConnectionConfigs | undefined;
import PeerButton from '@/components/PeerComponent';
import { dBTypeFromJSON } from '@/grpc_generated/peers';

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>
<PeerButton
peerName={mirrorConfig?.source?.name ?? ''}
peerType={dBTypeFromJSON(mirrorConfig?.source?.type)}
/>
</div>
</div>
<div className='basis-1/4 md:basis-1/3'>
<div>
<Label variant='subheadline' colorName='lowContrast'>
Destination
</Label>
</div>
<div>
<PeerButton
peerName={mirrorConfig?.destination?.name ?? ''}
peerType={dBTypeFromJSON(mirrorConfig?.destination?.type)}
/>
</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>
</div>
</>
);
}

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

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

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

import aggregateCountsByInterval from './aggregatedCountsByInterval';

const aggregateTypeMap: { [key: string]: string } = {
'15min': ' 15 mins',
hour: 'Hour',
day: 'Day',
month: 'Month',
};

function CdcGraph({ syncs }: { syncs: SyncStatusRow[] }) {
let [aggregateType, setAggregateType] = useState('hour');
const initialCount: [string, number][] = [];
let [counts, setCounts] = useState(initialCount);

let rows = syncs.map((sync) => ({
timestamp: sync.startTime,
count: sync.numRows,
}));

useEffect(() => {
let counts = aggregateCountsByInterval(rows, aggregateType);
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): React.ReactNode {
return (
<button
className={
aggregateType === selectedAggregateType
? 'bg-gray-200 px-1 mx-1 rounded-md'
: 'px-1 mx-1'
}
onClick={() => setAggregateType(aggregateType)}
>
{aggregateTypeMap[aggregateType]}
</button>
);
}

function formatGraphLabel(date: Date, aggregateType: String): 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');
default:
return 'Unknown aggregate type: ' + aggregateType;
}
}

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

function GraphBar({ label, count }: GraphBarProps) {
let color =
count && count > 0 ? 'bg-positive-fill-normal' : 'bg-base-border-subtle';
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>{numberWithCommas(count)}</div>
</div>
</div>
</div>
);
}

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

export default CdcGraph;
Loading

0 comments on commit de62965

Please sign in to comment.