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(