diff --git a/src/components/ui/Card.tsx b/src/components/ui/Card.tsx index b6b2fce..9d08b17 100644 --- a/src/components/ui/Card.tsx +++ b/src/components/ui/Card.tsx @@ -1,11 +1,88 @@ import classNames from "classnames"; -import { ReactElement, ReactNode } from "react"; +import React, { ReactElement, ReactNode } from "react"; import { useAnchoredElement } from "../../hooks/useAnchoredElement"; import { CardActionsMenu, CardAction } from "./CardActionsMenu"; import { CardAnchorLink } from "./CardAnchorLink"; export type { CardAction }; +function groupFieldsByColumn( + fields: ReactNode[], + columnCount: number, +): ReactNode[][] { + const columns: ReactNode[][] = Array.from({ length: columnCount }, () => []); + const fieldsPerColumn = Math.ceil(fields.length / columnCount); + fields.forEach((field, index) => { + columns[Math.floor(index / fieldsPerColumn)].push(field); + }); + return columns; +} + +function FieldsColumnGroup({ + fields, + columnCount, + className, +}: { + fields: ReactNode[]; + columnCount: number; + className?: string; +}): ReactElement { + const columns = groupFieldsByColumn(fields, columnCount); + + return ( +
+ {columns.map((columnFields, index) => ( +
+ {columnFields} +
+ ))} +
+ ); +} + +function ResponsiveFieldsBody({ + children, +}: { + children: ReactNode; +}): ReactElement { + const fields = React.Children.toArray(children); + + return ( + <> +
+ {fields} +
+ + + + ); +} + export function Card({ title, children, @@ -16,14 +93,14 @@ export function Card({ className, variant = "fields", }: { - title: string; + title: ReactNode; children: ReactNode; actions?: CardAction[]; headerControls?: ReactNode; afterChildren?: ReactNode; anchorId?: string; className?: string; - variant?: "fields" | "block"; + variant?: "fields" | "responsive-fields" | "block"; }): ReactElement { const { ref, highlighted } = useAnchoredElement(anchorId ?? ""); const cardActions = actions ?? []; @@ -63,6 +140,8 @@ export function Card({
{children}
+ ) : variant === "responsive-fields" ? ( + {children} ) : ( children )} @@ -80,8 +159,8 @@ export function Field({ }): ReactElement { return ( <> -
{label}
-
{children}
+
{label}
+
{children}
); } diff --git a/src/pages/Tables.tsx b/src/pages/Tables.tsx index 9904d85..166caa5 100644 --- a/src/pages/Tables.tsx +++ b/src/pages/Tables.tsx @@ -1,16 +1,13 @@ import { ReactElement, useEffect, useState, useRef } from "react"; -import { useSearchParams } from "react-router-dom"; -import { - CommonTable, - Column, - CellPrimitive, -} from "../components/ui/CommonTable"; +import { useNavigate, useSearchParams } from "react-router-dom"; +import classNames from "classnames"; import { DropdownFilter } from "../components/core/DropdownFilter"; import { TextFilter } from "../components/core/TextFilter"; import { getTableList } from "../clients/admin/sdk.gen"; import type { GetTableListResponse, TableListItem, + TableProgress, ValidationError, } from "../clients/admin/types.gen"; import { Loading } from "../components/core/Loading"; @@ -19,6 +16,8 @@ import { useDataFetching } from "../hooks/useDataFetching"; import { Pagination } from "../components/ui/Pagination"; import { adminClient } from "../clients/config"; import { Link } from "../components/core/Link"; +import { getSourceLink } from "../components/catalogs/CatalogCard"; +import { Card, CardAction, Field } from "../components/ui/Card"; const SEARCH_DEBOUNCE_MS = 300; @@ -97,35 +96,118 @@ function formatModificationDate(isoString: string): string { .replace(",", ""); } -function TablesResults({ data, loading }: TablesResultsProps): ReactElement { - const columns: Column[] = [ +function formatProgressPercent(count: number, total: number): string { + if (total <= 0) { + return "—"; + } + return `${Math.floor((count / total) * 100)}%`; +} + +function formatCatalogsSummary( + catalogs: TableProgress["catalogs"], + total: number, +): string { + if (total <= 0) { + return "—"; + } + + const parts = Object.entries(catalogs) + .map(([name, { structured }]) => ({ + name, + percent: Math.floor((structured / total) * 100), + })) + .filter(({ percent }) => percent > 0) + .map(({ name, percent }) => `${name} (${percent}%)`); + + return parts.length > 0 ? parts.join(", ") : "—"; +} + +function crossmatchListHref(tableName: string): string { + return `/crossmatch?table_name=${encodeURIComponent(tableName)}&triage_status=pending`; +} + +function TableListCard({ table }: { table: TableListItem }): ReactElement { + const navigate = useNavigate(); + const { progress } = table; + const total = progress.total_records; + const actions: CardAction[] = [ { - name: "Name", - renderCell: (value: CellPrimitive) => { - if (typeof value === "string") { - return {value}; - } - return ; - }, + title: "View crossmatch results", + onClick: () => navigate(crossmatchListHref(table.name)), }, - { name: "Description" }, - { name: "Number of records" }, - { name: "Number of columns" }, - { name: "Modification date" }, ]; - const tableData: Record[] = - data?.tables.map((table: TableListItem) => ({ - Name: table.name, - Description: table.description, - "Number of records": table.num_entries, - "Number of columns": table.num_fields, - "Modification date": table.modification_dt - ? formatModificationDate(table.modification_dt) - : "—", - })) ?? []; - - return ; + return ( + + {table.description || "—"} + + } + className="w-full" + variant="responsive-fields" + actions={actions} + > + + {table.name} + + + {table.bibcode ? ( + + {table.bibcode} + + ) : ( + "—" + )} + + {table.num_entries} + {table.num_fields} + + {table.modification_dt + ? formatModificationDate(table.modification_dt) + : "—"} + + + {formatProgressPercent(progress.unprocessed, total)} + + + {formatProgressPercent(progress.pending_triage, total)} + + + {formatProgressPercent(progress.resolved_unsubmitted, total)} + + + {formatProgressPercent(progress.submitted, total)} + + + {formatCatalogsSummary(progress.catalogs, total)} + + + ); +} + +function TablesResults({ data, loading }: TablesResultsProps): ReactElement { + const tables = data?.tables ?? []; + + return ( +
+
+ {tables.map((table) => ( + + ))} +
+ {loading && ( +
+ +
+ )} +
+ ); } async function fetcher(