Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mirror Overview UI #664

Merged
merged 10 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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':
serprex marked this conversation as resolved.
Show resolved Hide resolved
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);
}
};
serprex marked this conversation as resolved.
Show resolved Hide resolved

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
Loading