From 93471fc72552c2ff4fda2bfe285e3d0cbe26fb2f Mon Sep 17 00:00:00 2001 From: kraysent Date: Tue, 2 Jun 2026 15:00:52 +0200 Subject: [PATCH 1/3] more stuff on table list page directly --- src/components/ui/Card.tsx | 2 +- src/pages/Tables.tsx | 143 +++++++++++++++++++++++++++++-------- 2 files changed, 113 insertions(+), 32 deletions(-) diff --git a/src/components/ui/Card.tsx b/src/components/ui/Card.tsx index b6b2fce..701c3fc 100644 --- a/src/components/ui/Card.tsx +++ b/src/components/ui/Card.tsx @@ -16,7 +16,7 @@ export function Card({ className, variant = "fields", }: { - title: string; + title: ReactNode; children: ReactNode; actions?: CardAction[]; headerControls?: ReactNode; diff --git a/src/pages/Tables.tsx b/src/pages/Tables.tsx index 9904d85..8018b93 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,117 @@ 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" + 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( From aa2b83d3f57d4298303869e4087c4f5ebf6833d6 Mon Sep 17 00:00:00 2001 From: kraysent Date: Tue, 2 Jun 2026 15:07:32 +0200 Subject: [PATCH 2/3] a bit more thoughtful spacing --- src/components/ui/Card.tsx | 17 ++++++++++++++--- src/pages/Tables.tsx | 1 + 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/components/ui/Card.tsx b/src/components/ui/Card.tsx index 701c3fc..730ffd6 100644 --- a/src/components/ui/Card.tsx +++ b/src/components/ui/Card.tsx @@ -23,7 +23,7 @@ export function Card({ 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 +63,17 @@ export function Card({
{children}
+ ) : variant === "responsive-fields" ? ( +
+ {children} +
) : ( children )} @@ -80,8 +91,8 @@ export function Field({ }): ReactElement { return ( <> -
{label}
-
{children}
+
{label}
+
{children}
); } diff --git a/src/pages/Tables.tsx b/src/pages/Tables.tsx index 8018b93..166caa5 100644 --- a/src/pages/Tables.tsx +++ b/src/pages/Tables.tsx @@ -145,6 +145,7 @@ function TableListCard({ table }: { table: TableListItem }): ReactElement { } className="w-full" + variant="responsive-fields" actions={actions} > From a3f05d94a29eefea1b7330e2bedbb3f5cc143ea9 Mon Sep 17 00:00:00 2001 From: kraysent Date: Tue, 2 Jun 2026 15:18:00 +0200 Subject: [PATCH 3/3] fix layout --- src/components/ui/Card.tsx | 90 +++++++++++++++++++++++++++++++++----- 1 file changed, 79 insertions(+), 11 deletions(-) diff --git a/src/components/ui/Card.tsx b/src/components/ui/Card.tsx index 730ffd6..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, @@ -64,16 +141,7 @@ export function Card({ {children} ) : variant === "responsive-fields" ? ( -
- {children} -
+ {children} ) : ( children )}