diff --git a/ui/app/mirrors/edit/[mirrorId]/aggregatedCountsByInterval.ts b/ui/app/mirrors/edit/[mirrorId]/aggregatedCountsByInterval.ts index b9b3216625..c156b380b0 100644 --- a/ui/app/mirrors/edit/[mirrorId]/aggregatedCountsByInterval.ts +++ b/ui/app/mirrors/edit/[mirrorId]/aggregatedCountsByInterval.ts @@ -36,7 +36,15 @@ function aggregateCountsByInterval( // Iterate through the timestamps and populate the aggregatedCounts object for (let { timestamp, count } of timestamps) { - const date = roundUpToNearestNMinutes(timestamp, 1); + let N = 1; + if (interval === '1min') { + N = 1; + } else if (interval === '5min') { + N = 5; + } else if (interval === '15min') { + N = 15; + } + const date = roundUpToNearestNMinutes(timestamp, N); const formattedTimestamp = moment(date).format(timeUnit); if (!aggregatedCounts[formattedTimestamp]) { diff --git a/ui/app/mirrors/edit/[mirrorId]/cdc.tsx b/ui/app/mirrors/edit/[mirrorId]/cdc.tsx index 7d61280747..4afeedd5d4 100644 --- a/ui/app/mirrors/edit/[mirrorId]/cdc.tsx +++ b/ui/app/mirrors/edit/[mirrorId]/cdc.tsx @@ -105,6 +105,7 @@ export const SnapshotStatusTable = ({ status }: SnapshotStatusProps) => { const [currentPage, setCurrentPage] = useState(1); const totalPages = Math.ceil(allRows.length / ROWS_PER_PAGE); const [searchQuery, setSearchQuery] = useState(''); + const [sortDir, setSortDir] = useState<'asc' | 'dsc'>('asc'); const displayedRows = useMemo(() => { const shownRows = allRows.filter((row: any) => row.tableName.toLowerCase().includes(searchQuery.toLowerCase()) @@ -117,9 +118,9 @@ export const SnapshotStatusTable = ({ status }: SnapshotStatusProps) => { } if (aValue < bValue) { - return -1; + return sortDir === 'dsc' ? 1 : -1; } else if (aValue > bValue) { - return 1; + return sortDir === 'dsc' ? -1 : 1; } else { return 0; } @@ -130,7 +131,7 @@ export const SnapshotStatusTable = ({ status }: SnapshotStatusProps) => { return shownRows.length > ROWS_PER_PAGE ? shownRows.slice(startRow, endRow) : shownRows; - }, [allRows, currentPage, searchQuery, sortField]); + }, [allRows, currentPage, searchQuery, sortField, sortDir]); const handlePrevPage = () => { if (currentPage > 1) { @@ -183,6 +184,22 @@ export const SnapshotStatusTable = ({ status }: SnapshotStatusProps) => { }} defaultValue={{ value: 'cloneStartTime', label: 'Start Time' }} /> + + ), right: ( diff --git a/ui/app/mirrors/edit/[mirrorId]/cdcGraph.tsx b/ui/app/mirrors/edit/[mirrorId]/cdcGraph.tsx index 951baeaa3c..e241a07ffc 100644 --- a/ui/app/mirrors/edit/[mirrorId]/cdcGraph.tsx +++ b/ui/app/mirrors/edit/[mirrorId]/cdcGraph.tsx @@ -1,9 +1,11 @@ 'use client'; +import { formatGraphLabel, timeOptions } from '@/app/utils/graph'; import { Label } from '@/lib/Label'; import { BarChart } from '@tremor/react'; -import moment from 'moment'; import { useEffect, useState } from 'react'; import ReactSelect from 'react-select'; +import aggregateCountsByInterval from './aggregatedCountsByInterval'; + type SyncStatusRow = { batchId: number; startTime: Date; @@ -11,8 +13,6 @@ type SyncStatusRow = { numRows: number; }; -import aggregateCountsByInterval from './aggregatedCountsByInterval'; - function CdcGraph({ syncs }: { syncs: SyncStatusRow[] }) { let [aggregateType, setAggregateType] = useState('hour'); const initialCount: [string, number][] = []; @@ -30,14 +30,6 @@ function CdcGraph({ syncs }: { syncs: SyncStatusRow[] }) { setCounts(counts); }, [aggregateType, syncs]); - const timeOptions = [ - { label: '1min', value: '1min' }, - { label: '5min', value: '5min' }, - { label: '15min', value: '15min' }, - { label: 'hour', value: 'hour' }, - { label: 'day', value: 'day' }, - { label: 'month', value: 'month' }, - ]; return (
@@ -65,20 +57,4 @@ function CdcGraph({ syncs }: { syncs: SyncStatusRow[] }) { ); } -function formatGraphLabel(date: Date, aggregateType: String): string { - switch (aggregateType) { - case '1min': - case '5min': - case '15min': - 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; - } -} - export default CdcGraph; diff --git a/ui/app/mirrors/edit/[mirrorId]/syncStatusTable.tsx b/ui/app/mirrors/edit/[mirrorId]/syncStatusTable.tsx index 581144a7ef..e9b11bfa2d 100644 --- a/ui/app/mirrors/edit/[mirrorId]/syncStatusTable.tsx +++ b/ui/app/mirrors/edit/[mirrorId]/syncStatusTable.tsx @@ -50,6 +50,7 @@ function TimeWithDurationOrRunning({ const ROWS_PER_PAGE = 5; const sortOptions = [ + { value: 'batchId', label: 'Batch ID' }, { value: 'startTime', label: 'Start Time' }, { value: 'endTime', label: 'End Time' }, { value: 'numRows', label: 'Rows Synced' }, @@ -57,9 +58,10 @@ const sortOptions = [ export const SyncStatusTable = ({ rows }: SyncStatusTableProps) => { const [currentPage, setCurrentPage] = useState(1); const [sortField, setSortField] = useState< - 'startTime' | 'endTime' | 'numRows' - >('startTime'); + 'startTime' | 'endTime' | 'numRows' | 'batchId' + >('batchId'); + const [sortDir, setSortDir] = useState<'asc' | 'dsc'>('dsc'); const totalPages = Math.ceil(rows.length / ROWS_PER_PAGE); const [searchQuery, setSearchQuery] = useState(''); const displayedRows = useMemo(() => { @@ -75,9 +77,9 @@ export const SyncStatusTable = ({ rows }: SyncStatusTableProps) => { } if (aValue < bValue) { - return -1; + return sortDir === 'dsc' ? 1 : -1; } else if (aValue > bValue) { - return 1; + return sortDir === 'dsc' ? -1 : 1; } else { return 0; } @@ -88,7 +90,7 @@ export const SyncStatusTable = ({ rows }: SyncStatusTableProps) => { return shownRows.length > ROWS_PER_PAGE ? shownRows.slice(startRow, endRow) : shownRows; - }, [searchQuery, currentPage, rows, sortField]); + }, [searchQuery, currentPage, rows, sortField, sortDir]); const handlePrevPage = () => { if (currentPage > 1) { @@ -130,12 +132,31 @@ export const SyncStatusTable = ({ rows }: SyncStatusTableProps) => { }} onChange={(val, _) => { const sortVal = - (val?.value as 'startTime' | 'endTime' | 'numRows') ?? - 'startTime'; + (val?.value as + | 'startTime' + | 'endTime' + | 'numRows' + | 'batchId') ?? 'batchId'; setSortField(sortVal); }} - defaultValue={{ value: 'startTime', label: 'Start Time' }} + defaultValue={{ value: 'batchId', label: 'Batch ID' }} /> + + ), right: ( diff --git a/ui/app/mirrors/status/qrep/[mirrorId]/page.tsx b/ui/app/mirrors/status/qrep/[mirrorId]/page.tsx index 1f616421a9..846bcf70f0 100644 --- a/ui/app/mirrors/status/qrep/[mirrorId]/page.tsx +++ b/ui/app/mirrors/status/qrep/[mirrorId]/page.tsx @@ -2,6 +2,7 @@ import prisma from '@/app/utils/prisma'; import { Header } from '@/lib/Header'; import { LayoutMain } from '@/lib/Layout'; import QRepConfigViewer from './qrepConfigViewer'; +import QrepGraph from './qrepGraph'; import QRepStatusTable, { QRepPartitionStatus } from './qrepStatusTable'; export const dynamic = 'force-dynamic'; @@ -44,6 +45,15 @@ export default async function QRepMirrorStatus({
{mirrorId}
+ ({ + partitionID: partition.partitionId, + startTime: partition.startTime, + endTime: partition.endTime, + numRows: partition.numRows, + }))} + /> +

); diff --git a/ui/app/mirrors/status/qrep/[mirrorId]/qrepConfigViewer.tsx b/ui/app/mirrors/status/qrep/[mirrorId]/qrepConfigViewer.tsx index ba8c85ce92..31f54b384d 100644 --- a/ui/app/mirrors/status/qrep/[mirrorId]/qrepConfigViewer.tsx +++ b/ui/app/mirrors/status/qrep/[mirrorId]/qrepConfigViewer.tsx @@ -38,7 +38,7 @@ export default async function QRepConfigViewer({ return (
- +
{qrepConfig.initialCopyOnly ? 'Initial Load' : 'Continuous Sync'} diff --git a/ui/app/mirrors/status/qrep/[mirrorId]/qrepGraph.tsx b/ui/app/mirrors/status/qrep/[mirrorId]/qrepGraph.tsx new file mode 100644 index 0000000000..510c473db7 --- /dev/null +++ b/ui/app/mirrors/status/qrep/[mirrorId]/qrepGraph.tsx @@ -0,0 +1,60 @@ +'use client'; +import { formatGraphLabel, timeOptions } from '@/app/utils/graph'; +import { Label } from '@/lib/Label'; +import { BarChart } from '@tremor/react'; +import { useEffect, useState } from 'react'; +import ReactSelect from 'react-select'; +import aggregateCountsByInterval from '../../../edit/[mirrorId]/aggregatedCountsByInterval'; + +type QrepStatusRow = { + partitionID: string; + startTime: Date | null; + endTime: Date | null; + numRows: number | null; +}; + +function QrepGraph({ syncs }: { syncs: QrepStatusRow[] }) { + let [aggregateType, setAggregateType] = useState('hour'); + const initialCount: [string, number][] = []; + let [counts, setCounts] = useState(initialCount); + + useEffect(() => { + let rows = syncs.map((sync) => ({ + timestamp: sync.startTime!, + count: sync.numRows ?? 0, + })); + + let counts = aggregateCountsByInterval(rows, aggregateType); + counts = counts.slice(0, 29); + counts = counts.reverse(); + setCounts(counts); + }, [aggregateType, syncs]); + + return ( +
+
+ val && setAggregateType(val.value)} + /> +
+
+ +
+ ({ + name: formatGraphLabel(new Date(count[0]), aggregateType), + 'Rows synced at a point in time': count[1], + }))} + index='name' + categories={['Rows synced at a point in time']} + /> +
+ ); +} + +export default QrepGraph; diff --git a/ui/app/mirrors/status/qrep/[mirrorId]/qrepStatusTable.tsx b/ui/app/mirrors/status/qrep/[mirrorId]/qrepStatusTable.tsx index 077cf60b0b..a405835fb3 100644 --- a/ui/app/mirrors/status/qrep/[mirrorId]/qrepStatusTable.tsx +++ b/ui/app/mirrors/status/qrep/[mirrorId]/qrepStatusTable.tsx @@ -45,10 +45,14 @@ function RowPerPartition({ return ( - + - + @@ -107,7 +111,7 @@ export default function QRepStatusTable({ return ( Progress} + title={} toolbar={{ left: ( <> @@ -131,9 +135,6 @@ export default function QRepStatusTable({ > - @@ -146,7 +147,7 @@ export default function QRepStatusTable({ ), right: ( ) => setSearchQuery(e.target.value) } diff --git a/ui/app/utils/graph.ts b/ui/app/utils/graph.ts new file mode 100644 index 0000000000..bf80e35438 --- /dev/null +++ b/ui/app/utils/graph.ts @@ -0,0 +1,26 @@ +import moment from 'moment'; + +export const timeOptions = [ + { label: '1min', value: '1min' }, + { label: '5min', value: '5min' }, + { label: '15min', value: '15min' }, + { label: 'hour', value: 'hour' }, + { label: 'day', value: 'day' }, + { label: 'month', value: 'month' }, +]; + +export function formatGraphLabel(date: Date, aggregateType: String): string { + switch (aggregateType) { + case '1min': + case '5min': + case '15min': + 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; + } +}