Skip to content

Commit

Permalink
Mirror Overview UI (#664)
Browse files Browse the repository at this point in the history
Fixes #654 
Fixes #660 
Fixes #656 

- Restores tab UI for specific mirror page (`/edit/[mirrorId]`)
- Adds mirror configuration details and list of source tables to
destination tables in overview tab (with search bar)
- Adds sort functionality for CDC tab and Initial load tab. In CDC you
can sort by `Start Time`, `End Time` and `Rows Synced`. In Initial Load
you can sort by `Start Time` and `Time per partition`.
- Timeframe options in CDC Graph now in a dropdown.
- Moves Sync History graph to Sync tab.
- Displays further mirror config via a `View More` button click which
pops up a modal with the info.
- Centers the drop modal (same component as configuration modal in
specific mirror page).

<img width="1724" alt="Screenshot 2023-11-16 at 1 59 44 AM"
src="https://github.com/PeerDB-io/peerdb/assets/65964360/db11bc6e-2f3a-4fed-832d-fba9c74cd171">

<img width="1724" alt="Screenshot 2023-11-16 at 2 00 30 AM"
src="https://github.com/PeerDB-io/peerdb/assets/65964360/4cc8f648-bcec-4e61-b7ba-db2a7ca30daa">
  • Loading branch information
Amogh-Bharadwaj authored Nov 16, 2023
1 parent e9c028d commit ce45d28
Show file tree
Hide file tree
Showing 17 changed files with 591 additions and 134 deletions.
5 changes: 3 additions & 2 deletions ui/app/mirrors/create/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ export const flowNameSchema = z
required_error: 'Mirror name is required.',
})
.min(1, { message: 'Mirror name cannot be empty.' })
.regex(/^[\w]*$/, {
message: 'Mirror name must contain only letters, numbers and underscores',
.regex(/^[a-z0-9_]*$/, {
message:
'Mirror name must contain only lowercase letters, numbers and underscores',
});

export const tableMappingSchema = z
Expand Down
19 changes: 13 additions & 6 deletions ui/app/mirrors/edit/[mirrorId]/aggregatedCountsByInterval.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ function aggregateCountsByInterval(
case 'day':
timeUnit = 'YYYY-MM-DD';
break;
case '1min':
case '5min':
timeUnit = 'YYYY-MM-DD HH:mm';
break;
default:
throw new Error('Invalid interval provided');
}
Expand All @@ -32,7 +36,7 @@ function aggregateCountsByInterval(

// Iterate through the timestamps and populate the aggregatedCounts object
for (let { timestamp, count } of timestamps) {
const date = roundUpToNearest15Minutes(timestamp);
const date = roundUpToNearestNMinutes(timestamp, 15);
const formattedTimestamp = moment(date).format(timeUnit);

if (!aggregatedCounts[formattedTimestamp]) {
Expand All @@ -48,7 +52,10 @@ function aggregateCountsByInterval(
let currentTimestamp = new Date();

if (interval === '15min') {
currentTimestamp = roundUpToNearest15Minutes(currentTimestamp);
currentTimestamp = roundUpToNearestNMinutes(currentTimestamp, 15);
}
if (interval === '5min') {
currentTimestamp = roundUpToNearestNMinutes(currentTimestamp, 5);
}

while (intervals.length < 30) {
Expand All @@ -73,13 +80,13 @@ function aggregateCountsByInterval(
return resultArray;
}

function roundUpToNearest15Minutes(date: Date) {
function roundUpToNearestNMinutes(date: Date, N: number) {
const minutes = date.getMinutes();
const remainder = minutes % 15;
const remainder = minutes % N;

if (remainder > 0) {
// Round up to the nearest 15 minutes
date.setMinutes(minutes + (15 - remainder));
// Round up to the nearest N minutes
date.setMinutes(minutes + (N - remainder));
}

// Reset seconds and milliseconds to zero to maintain the same time
Expand Down
205 changes: 182 additions & 23 deletions ui/app/mirrors/edit/[mirrorId]/cdc.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
'use client';

import TimeLabel from '@/components/TimeComponent';
import { QRepMirrorStatus, SnapshotStatus } from '@/grpc_generated/route';
import {
CDCMirrorStatus,
QRepMirrorStatus,
SnapshotStatus,
} from '@/grpc_generated/route';
import { Button } from '@/lib/Button';
import { Icon } from '@/lib/Icon';
import { Label } from '@/lib/Label';
import { ProgressBar } from '@/lib/ProgressBar';
import { SearchField } from '@/lib/SearchField';
import { Table, TableCell, TableRow } from '@/lib/Table';
import * as Tabs from '@radix-ui/react-tabs';
import moment, { Duration, Moment } from 'moment';
import Link from 'next/link';
import { useMemo, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import ReactSelect from 'react-select';
import styled from 'styled-components';
import CdcDetails from './cdcDetails';

class TableCloneSummary {
flowJobName: string;
Expand Down Expand Up @@ -87,37 +95,94 @@ function summarizeTableClone(clone: QRepMirrorStatus): TableCloneSummary {
type SnapshotStatusProps = {
status: SnapshotStatus;
};

const ROWS_PER_PAGE = 5;
export const SnapshotStatusTable = ({ status }: SnapshotStatusProps) => {
const [sortField, setSortField] = useState<
'cloneStartTime' | 'avgTimePerPartition'
>('cloneStartTime');
const allRows = status.clones.map(summarizeTableClone);
const [currentPage, setCurrentPage] = useState(1);
const totalPages = Math.ceil(allRows.length / ROWS_PER_PAGE);
const [searchQuery, setSearchQuery] = useState<string>('');
const snapshotRows = useMemo(
() =>
status.clones
.map(summarizeTableClone)
.filter((row: any) =>
row.tableName.toLowerCase().includes(searchQuery.toLowerCase())
),
[status.clones, searchQuery]
);
const displayedRows = useMemo(() => {
const shownRows = allRows.filter((row: any) =>
row.tableName.toLowerCase().includes(searchQuery.toLowerCase())
);
shownRows.sort((a, b) => {
const aValue = a[sortField];
const bValue = b[sortField];
if (aValue === null || bValue === null) {
return 0;
}

if (aValue < bValue) {
return -1;
} else if (aValue > bValue) {
return 1;
} else {
return 0;
}
});

const startRow = (currentPage - 1) * ROWS_PER_PAGE;
const endRow = startRow + ROWS_PER_PAGE;
return shownRows.length > ROWS_PER_PAGE
? shownRows.slice(startRow, endRow)
: shownRows;
}, [allRows, currentPage, searchQuery, sortField]);

const handlePrevPage = () => {
if (currentPage > 1) {
setCurrentPage(currentPage - 1);
}
};

const handleNextPage = () => {
if (currentPage < totalPages) {
setCurrentPage(currentPage + 1);
}
};

const sortOptions = [
{ value: 'cloneStartTime', label: 'Start Time' },
{ value: 'avgTimePerPartition', label: 'Time Per Partition' },
];
return (
<div style={{ marginTop: '2rem' }}>
<Table
title={<Label variant='headline'>Initial Copy</Label>}
toolbar={{
left: (
<>
<Button variant='normalBorderless'>
<Button variant='normalBorderless' onClick={handlePrevPage}>
<Icon name='chevron_left' />
</Button>
<Button variant='normalBorderless'>
<Button variant='normalBorderless' onClick={handleNextPage}>
<Icon name='chevron_right' />
</Button>
<Label>{`${currentPage} of ${totalPages}`}</Label>
<Button
variant='normalBorderless'
onClick={() => window.location.reload()}
>
<Icon name='refresh' />
</Button>
<ReactSelect
options={sortOptions}
onChange={(val, _) => {
const sortVal =
(val?.value as 'cloneStartTime' | 'avgTimePerPartition') ??
'cloneStartTime';
setSortField(sortVal);
}}
value={{
value: sortField,
label: sortOptions.find((opt) => opt.value === sortField)
?.label,
}}
defaultValue={{ value: 'cloneStartTime', label: 'Start Time' }}
/>
</>
),
right: (
Expand All @@ -139,7 +204,7 @@ export const SnapshotStatusTable = ({ status }: SnapshotStatusProps) => {
</TableRow>
}
>
{snapshotRows.map((clone, index) => (
{displayedRows.map((clone, index) => (
<TableRow key={index}>
<TableCell>
<Label>
Expand All @@ -152,16 +217,11 @@ export const SnapshotStatusTable = ({ status }: SnapshotStatusProps) => {
</Label>
</TableCell>
<TableCell>
<Label>
{
<TimeLabel
timeVal={
clone.cloneStartTime?.format('YYYY-MM-DD HH:mm:ss') ||
'N/A'
}
/>
<TimeLabel
timeVal={
clone.cloneStartTime?.format('YYYY-MM-DD HH:mm:ss') || 'N/A'
}
</Label>
/>
</TableCell>
<TableCell>
<ProgressBar progress={clone.getPartitionProgressPercentage()} />
Expand All @@ -179,3 +239,102 @@ export const SnapshotStatusTable = ({ status }: SnapshotStatusProps) => {
</div>
);
};

const Trigger = styled(
({ isActive, ...props }: { isActive?: boolean } & Tabs.TabsTriggerProps) => (
<Tabs.Trigger {...props} />
)
)<{ isActive?: boolean }>`
background-color: ${({ theme, isActive }) =>
isActive ? theme.colors.accent.surface.selected : 'white'};
font-weight: ${({ isActive }) => (isActive ? 'bold' : 'normal')};
&:hover {
color: ${({ theme }) => theme.colors.accent.text.highContrast};
}
`;

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

type CDCMirrorStatusProps = {
cdc: CDCMirrorStatus;
rows: SyncStatusRow[];
createdAt?: Date;
syncStatusChild?: React.ReactNode;
};
export function CDCMirror({
cdc,
rows,
createdAt,
syncStatusChild,
}: CDCMirrorStatusProps) {
const [selectedTab, setSelectedTab] = useState('');

let snapshot = <></>;
if (cdc.snapshotStatus) {
snapshot = <SnapshotStatusTable status={cdc.snapshotStatus} />;
}

const handleTab = (tabVal: string) => {
localStorage.setItem('mirrortab', tabVal);
setSelectedTab(tabVal);
};

useEffect(() => {
if (typeof window !== 'undefined') {
setSelectedTab(localStorage?.getItem('mirrortab') || 'tab1');
}
}, []);

return (
<Tabs.Root
className='flex flex-col w-full'
value={selectedTab}
onValueChange={(val) => handleTab(val)}
style={{ marginTop: '2rem' }}
>
<Tabs.List className='flex border-b' aria-label='Details'>
<Trigger
isActive={selectedTab === 'tab1'}
className='flex-1 px-5 h-[45px] flex items-center justify-center text-sm focus:shadow-outline focus:outline-none'
value='tab1'
>
Overview
</Trigger>
<Trigger
isActive={selectedTab === 'tab2'}
className='flex-1 px-5 h-[45px] flex items-center justify-center text-sm focus:shadow-outline focus:outline-none'
value='tab2'
>
Sync Status
</Trigger>
<Trigger
isActive={selectedTab === 'tab3'}
className='flex-1 px-5 h-[45px] flex items-center justify-center text-sm focus:shadow-outline focus:outline-none'
value='tab3'
>
Initial Copy
</Trigger>
</Tabs.List>
<Tabs.Content className='p-5 rounded-b-md' value='tab1'>
<CdcDetails
syncs={rows}
createdAt={createdAt}
mirrorConfig={cdc.config}
/>
</Tabs.Content>
<Tabs.Content className='p-5 rounded-b-md' value='tab2'>
{syncStatusChild}
</Tabs.Content>
<Tabs.Content className='p-5 rounded-b-md' value='tab3'>
{snapshot}
</Tabs.Content>
</Tabs.Root>
);
}
Loading

0 comments on commit ce45d28

Please sign in to comment.