-
Notifications
You must be signed in to change notification settings - Fork 97
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feature: add screen to view mirror activity (#627)
- Loading branch information
1 parent
1dcfb09
commit de62965
Showing
7 changed files
with
367 additions
and
33 deletions.
There are no files selected for viewing
92 changes: 92 additions & 0 deletions
92
ui/app/mirrors/edit/[mirrorId]/aggregatedCountsByInterval.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.