diff --git a/ui/app/api/mirrors/status/route.ts b/ui/app/api/mirrors/status/route.ts deleted file mode 100644 index c15ccb0c92..0000000000 --- a/ui/app/api/mirrors/status/route.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { GetFlowHttpAddressFromEnv } from '@/rpc/http'; - -function getMirrorStatusUrl(mirrorId: string) { - let base = GetFlowHttpAddressFromEnv(); - return `${base}/v1/mirrors/${mirrorId}`; -} - -export async function POST(request: Request) { - const { flowJobName } = await request.json(); - const url = getMirrorStatusUrl(flowJobName); - const resp = await fetch(url); - const json = await resp.json(); - return new Response(JSON.stringify(json)); -} diff --git a/ui/app/mirrors/edit/[mirrorId]/cdc.tsx b/ui/app/mirrors/edit/[mirrorId]/cdc.tsx index 7d46c6714d..caa5226663 100644 --- a/ui/app/mirrors/edit/[mirrorId]/cdc.tsx +++ b/ui/app/mirrors/edit/[mirrorId]/cdc.tsx @@ -5,7 +5,6 @@ import { QRepMirrorStatus, SnapshotStatus, } from '@/grpc_generated/route'; -import { Badge } from '@/lib/Badge'; import { Button } from '@/lib/Button'; import { Checkbox } from '@/lib/Checkbox'; import { Icon } from '@/lib/Icon'; @@ -13,27 +12,12 @@ 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'; - -const Badges = [ - - - Active - , - - - Paused - , - - - Broken - , - - - Incomplete - , -]; +import { useState } from 'react'; +import styled from 'styled-components'; +import CDCDetails from './cdcDetails'; class TableCloneSummary { flowJobName: string; @@ -125,12 +109,6 @@ const SnapshotStatusTable = ({ status }: SnapshotStatusProps) => ( - - ), right: , @@ -179,13 +157,69 @@ const SnapshotStatusTable = ({ status }: SnapshotStatusProps) => ( ); +const Trigger = styled(({ isActive, ...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 CDCMirrorStatusProps = { cdc: CDCMirrorStatus; + syncStatusChild?: React.ReactNode; }; -export function CDCMirror({ cdc }: CDCMirrorStatusProps) { +export function CDCMirror({ cdc, syncStatusChild }: CDCMirrorStatusProps) { + const [selectedTab, setSelectedTab] = useState('tab1'); + let snapshot = <>; if (cdc.snapshotStatus) { snapshot = ; } - return <>{snapshot}; + + return ( + + + + Details + + + Sync Status + + + Initial Copy + + + + + + + {syncStatusChild} + + + {snapshot} + + + ); } diff --git a/ui/app/mirrors/edit/[mirrorId]/cdcDetails.tsx b/ui/app/mirrors/edit/[mirrorId]/cdcDetails.tsx new file mode 100644 index 0000000000..bd20ad1851 --- /dev/null +++ b/ui/app/mirrors/edit/[mirrorId]/cdcDetails.tsx @@ -0,0 +1,35 @@ +import { FlowConnectionConfigs } from '@/grpc_generated/flow'; + +type CDCDetailsProps = { + config: FlowConnectionConfigs | undefined; +}; + +export default function CDCDetails({ config }: CDCDetailsProps) { + if (!config) { + return
No configuration provided
; + } + + return ( +
+

CDC Details

+
+ + + + + + + + + + + + + + + +
Source{config.source?.name || '-'}
Destination{config.destination?.name || '-'}
Flow Job Name{config.flowJobName}
+
+
+ ); +} diff --git a/ui/app/mirrors/edit/[mirrorId]/page.tsx b/ui/app/mirrors/edit/[mirrorId]/page.tsx index 6a7eff8637..6e8c9c013c 100644 --- a/ui/app/mirrors/edit/[mirrorId]/page.tsx +++ b/ui/app/mirrors/edit/[mirrorId]/page.tsx @@ -1,57 +1,55 @@ -'use client'; - import { MirrorStatusResponse } from '@/grpc_generated/route'; import { Header } from '@/lib/Header'; import { LayoutMain } from '@/lib/Layout'; -import { ProgressCircle } from '@/lib/ProgressCircle'; -import useSWR from 'swr'; +import { GetFlowHttpAddressFromEnv } from '@/rpc/http'; +import { Suspense } from 'react'; import { CDCMirror } from './cdc'; +import SyncStatus from './syncStatus'; type EditMirrorProps = { params: { mirrorId: string }; }; -async function fetcher([url, mirrorId]: [string, string]) { - return fetch(url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - flowJobName: mirrorId, - }), - }) - .then((res) => { - if (!res.ok) throw new Error('Error fetching mirror status'); - return res.json(); - }) - .then((res: MirrorStatusResponse) => res); +function getMirrorStatusUrl(mirrorId: string) { + let base = GetFlowHttpAddressFromEnv(); + return `${base}/v1/mirrors/${mirrorId}`; } -export default function EditMirror({ params: { mirrorId } }: EditMirrorProps) { - const { - data: mirrorStatus, - error, - isValidating, - } = useSWR(() => [`/api/mirrors/status`, mirrorId], fetcher); - - if (isValidating) { - return ; - } +async function getMirrorStatus(mirrorId: string) { + const url = getMirrorStatusUrl(mirrorId); + const resp = await fetch(url); + const json = await resp.json(); + return json; +} - if (error) { - console.error('Error fetching mirror status:', error); - return
Error occurred!
; - } +function Loading() { + return
Loading...
; +} +export default async function EditMirror({ + params: { mirrorId }, +}: EditMirrorProps) { + const mirrorStatus: MirrorStatusResponse = await getMirrorStatus(mirrorId); if (!mirrorStatus) { return
No mirror status found!
; } + let syncStatusChild = <>; + if (mirrorStatus.cdcStatus) { + syncStatusChild = ; + } + return (
{mirrorId}
- {mirrorStatus.cdcStatus && } + }> + {mirrorStatus.cdcStatus && ( + + )} +
); } diff --git a/ui/app/mirrors/edit/[mirrorId]/syncStatus.tsx b/ui/app/mirrors/edit/[mirrorId]/syncStatus.tsx new file mode 100644 index 0000000000..80cb35701c --- /dev/null +++ b/ui/app/mirrors/edit/[mirrorId]/syncStatus.tsx @@ -0,0 +1,33 @@ +import prisma from '@/app/utils/prisma'; +import { SyncStatusTable } from './syncStatusTable'; + +type SyncStatusProps = { + flowJobName: string | undefined; +}; + +export default async function SyncStatus({ flowJobName }: SyncStatusProps) { + if (!flowJobName) { + return
Flow job name not provided!
; + } + + const syncs = await prisma.cdc_batches.findMany({ + where: { + flow_name: flowJobName, + start_time: { + not: undefined, + }, + }, + orderBy: { + start_time: 'desc', + }, + }); + + const rows = syncs.map((sync) => ({ + batchId: sync.id, + startTime: sync.start_time, + endTime: sync.end_time, + numRows: sync.rows_in_batch, + })); + + return ; +} diff --git a/ui/app/mirrors/edit/[mirrorId]/syncStatusTable.tsx b/ui/app/mirrors/edit/[mirrorId]/syncStatusTable.tsx new file mode 100644 index 0000000000..64c319a4f9 --- /dev/null +++ b/ui/app/mirrors/edit/[mirrorId]/syncStatusTable.tsx @@ -0,0 +1,120 @@ +'use client'; + +import { Button } from '@/lib/Button'; +import { Checkbox } from '@/lib/Checkbox'; +import { Icon } from '@/lib/Icon'; +import { Label } from '@/lib/Label'; +import { ProgressCircle } from '@/lib/ProgressCircle'; +import { SearchField } from '@/lib/SearchField'; +import { Table, TableCell, TableRow } from '@/lib/Table'; +import moment from 'moment'; +import { useState } from 'react'; + +type SyncStatusRow = { + batchId: number; + startTime: Date; + endTime: Date | null; + numRows: number; +}; + +type SyncStatusTableProps = { + rows: SyncStatusRow[]; +}; + +function TimeWithDurationOrRunning({ + startTime, + endTime, +}: { + startTime: Date; + endTime: Date | null; +}) { + if (endTime) { + return ( + + ); + } else { + return ( + + ); + } +} + +const ROWS_PER_PAGE = 10; + +export const SyncStatusTable = ({ rows }: SyncStatusTableProps) => { + const [currentPage, setCurrentPage] = useState(1); + const totalPages = Math.ceil(rows.length / ROWS_PER_PAGE); + + const startRow = (currentPage - 1) * ROWS_PER_PAGE; + const endRow = startRow + ROWS_PER_PAGE; + + const displayedRows = rows.slice(startRow, endRow); + + const handlePrevPage = () => { + if (currentPage > 1) setCurrentPage(currentPage - 1); + }; + + const handleNextPage = () => { + if (currentPage < totalPages) setCurrentPage(currentPage + 1); + }; + + return ( + Initial Copy} + toolbar={{ + left: ( + <> + + + + + + ), + right: , + }} + header={ + + + + + Batch ID + Start Time + End Time (Duation) + Num Rows Synced + + } + > + {displayedRows.map((row, index) => ( + + + + + + + + + + + + + + {row.numRows} + + ))} +
+ ); +}; diff --git a/ui/lib/Table/Table.styles.ts b/ui/lib/Table/Table.styles.ts index 8b9375656f..09958cbf8d 100644 --- a/ui/lib/Table/Table.styles.ts +++ b/ui/lib/Table/Table.styles.ts @@ -19,7 +19,9 @@ export const StyledTable = styled.table` export const StyledTableBody = styled.tbody``; -export const StyledTableHeader = styled.thead``; +export const StyledTableHeader = styled.thead` + text-align: left; +`; export const ToolbarWrapper = styled.div` display: flex; diff --git a/ui/package.json b/ui/package.json index 72fa05ce48..c22337cf24 100644 --- a/ui/package.json +++ b/ui/package.json @@ -25,6 +25,7 @@ "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-select": "^1.2.2", "@radix-ui/react-switch": "^1.0.3", + "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-toast": "^1.1.4", "@radix-ui/react-toggle": "^1.0.3", "@radix-ui/react-toggle-group": "^1.0.4", diff --git a/ui/yarn.lock b/ui/yarn.lock index 7e366ef2d0..e7347d113c 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -2119,6 +2119,21 @@ "@radix-ui/react-use-previous" "1.0.1" "@radix-ui/react-use-size" "1.0.1" +"@radix-ui/react-tabs@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz#993608eec55a5d1deddd446fa9978d2bc1053da2" + integrity sha512-egZfYY/+wRNCflXNHx+dePvnz9FbmssDTJBtgRfDY7e8SE5oIo3Py2eCB1ckAbh1Q7cQ/6yJZThJ++sgbxibog== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-direction" "1.0.1" + "@radix-ui/react-id" "1.0.1" + "@radix-ui/react-presence" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-roving-focus" "1.0.4" + "@radix-ui/react-use-controllable-state" "1.0.1" + "@radix-ui/react-toast@^1.1.4": version "1.1.5" resolved "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.1.5.tgz"