From c68f1250009e824b422a31adaa4548bc7ebe7c87 Mon Sep 17 00:00:00 2001 From: kraysent Date: Sun, 8 Mar 2026 23:49:28 +0000 Subject: [PATCH 1/4] add records page --- src/App.tsx | 2 + src/assets/texts.json | 1 + src/pages/Records.tsx | 321 +++++++++++++++++++++++++++++++++++++ src/pages/TableDetails.tsx | 22 ++- 4 files changed, 342 insertions(+), 4 deletions(-) create mode 100644 src/pages/Records.tsx diff --git a/src/App.tsx b/src/App.tsx index 8d0bf2a..10b5bca 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,6 +5,7 @@ import { ObjectDetailsPage } from "./pages/ObjectDetails"; import { NotFoundPage } from "./pages/NotFound"; import { TableDetailsPage } from "./pages/TableDetails"; import { CrossmatchResultsPage } from "./pages/CrossmatchResults"; +import { RecordsPage } from "./pages/Records"; import { RecordCrossmatchDetailsPage } from "./pages/RecordCrossmatchDetails"; import { TablesPage } from "./pages/Tables"; import { Layout } from "./components/ui/Layout"; @@ -37,6 +38,7 @@ function App() { } /> } /> } /> + } /> void; +} + +function RecordsFilters({ + tableName, + triageStatus, + pageSize, + onApplyFilters, +}: RecordsFiltersProps): ReactElement { + const [localTriageStatus, setLocalTriageStatus] = useState( + triageStatus ?? "all", + ); + const [localPageSize, setLocalPageSize] = useState(pageSize); + const [localTableName, setLocalTableName] = useState(tableName || ""); + + useEffect(() => { + setLocalTriageStatus(triageStatus ?? "all"); + setLocalPageSize(pageSize); + setLocalTableName(tableName || ""); + }, [triageStatus, pageSize, tableName]); + + function applyFilters(): void { + onApplyFilters(localTableName, localTriageStatus, localPageSize); + } + + return ( +
+ + + + setLocalPageSize(parseInt(value))} + /> +
+ +
+
+ ); +} + +interface RecordsTableProps { + data: GetRecordsResponse | null; + loading?: boolean; + showCandidates?: boolean; +} + +function RecordsTable({ + data, + loading, + showCandidates = false, +}: RecordsTableProps): ReactElement { + function getRecordName(record: RecordType): ReactElement { + const displayName = record.catalogs?.designation?.name || record.id; + return {displayName}; + } + + function getTriageStatusLabel(status: CrossmatchTriageStatus): string { + return getResource(`crossmatch.triage.${status}`).Title; + } + + function renderCandidates(record: RecordType): ReactElement { + const pgcNumbers = record.crossmatch.candidates.map((c) => c.pgc); + return ( + <> + {pgcNumbers.map((pgc, index) => ( + + {pgc} + + ))} + + ); + } + + const columns: Column[] = [ + { + name: "Name", + renderCell: (recordIndex: CellPrimitive) => { + if (typeof recordIndex === "number" && data?.records[recordIndex]) { + return getRecordName(data.records[recordIndex]); + } + return ; + }, + }, + { + name: "Manual check status", + renderCell: (recordIndex: CellPrimitive) => { + if (typeof recordIndex === "number" && data?.records[recordIndex]) { + return getTriageStatusLabel( + data.records[recordIndex].crossmatch.triage_status, + ); + } + return ; + }, + }, + { + name: "Nature", + renderCell: (recordIndex: CellPrimitive) => { + if (typeof recordIndex === "number" && data?.records[recordIndex]) { + const typeName = + data.records[recordIndex].catalogs?.nature?.type_name; + return {typeName ?? "—"}; + } + return ; + }, + }, + ...(showCandidates + ? [ + { + name: "Candidates", + renderCell: (recordIndex: CellPrimitive) => { + if ( + typeof recordIndex === "number" && + data?.records[recordIndex] + ) { + return renderCandidates(data.records[recordIndex]); + } + return ; + }, + }, + ] + : []), + ]; + + const tableData: Record[] = + data?.records.map((_record: RecordType, index: number) => { + const row: Record = { + "Name": index, + "Manual check status": index, + "Nature": index, + }; + if (showCandidates) { + row["Candidates"] = index; + } + return row; + }) || []; + + return ; +} + +async function fetcher( + tableName: string | null, + triageStatus: CrossmatchTriageStatus | null, + page: number, + pageSize: number, +): Promise { + if (!tableName) { + throw new Error("Table name is required"); + } + + const response = await getRecords({ + client: adminClient, + query: { + table_name: tableName, + triage_status: triageStatus, + page, + page_size: pageSize, + }, + }); + + if (response.error) { + throw new Error( + response.error.detail + ?.map((err: ValidationError) => err.msg) + .join(", ") || "Failed to fetch records", + ); + } + + if (!response.data) { + throw new Error("No data received from server"); + } + + return response.data.data; +} + +export function RecordsPage(): ReactElement { + const [searchParams, setSearchParams] = useSearchParams(); + + const tableName = searchParams.get("table_name"); + const triageStatusParam = searchParams.get("triage_status"); + const apiTriageStatus: CrossmatchTriageStatus | null = + triageStatusParam === null || triageStatusParam === "" + ? null + : triageStatusParam === "all" + ? null + : (triageStatusParam as CrossmatchTriageStatus); + const page = parseInt(searchParams.get("page") || "0"); + const pageSize = parseInt(searchParams.get("page_size") || "25"); + + useEffect(() => { + document.title = `Records${tableName ? ` - ${tableName}` : ""} | HyperLEDA`; + }, [tableName]); + + const { data, loading, error } = useDataFetching( + () => fetcher(tableName, apiTriageStatus, page, pageSize), + [tableName, apiTriageStatus, page, pageSize], + ); + + function handlePageChange(newPage: number): void { + const newSearchParams = new URLSearchParams(searchParams); + newSearchParams.set("page", newPage.toString()); + setSearchParams(newSearchParams); + } + + function handleApplyFilters( + newTableName: string, + newTriageStatus: string, + newPageSize: number, + ): void { + const newSearchParams = new URLSearchParams(searchParams); + + if (newTableName.trim()) { + newSearchParams.set("table_name", newTableName.trim()); + } else { + newSearchParams.delete("table_name"); + } + + if (newTriageStatus === "all") { + newSearchParams.delete("triage_status"); + } else { + newSearchParams.set("triage_status", newTriageStatus); + } + + newSearchParams.set("page_size", newPageSize.toString()); + newSearchParams.set("page", "0"); + + setSearchParams(newSearchParams); + } + + function Content(): ReactElement { + if (error && !data) return ; + if (!data?.records && loading) return ; + if (!data?.records) return ; + + return ( + <> + + + + ); + } + + return ( + <> +

Records

+ + + + ); +} diff --git a/src/pages/TableDetails.tsx b/src/pages/TableDetails.tsx index 5eb2c47..4204e39 100644 --- a/src/pages/TableDetails.tsx +++ b/src/pages/TableDetails.tsx @@ -198,11 +198,25 @@ function CrossmatchStats(props: CrossmatchStatsProps): ReactElement { return ( -
+

Crossmatch Statistics

- +
+ + +
); From f43d5838743c7a22964bd6b119f67aa307ade53c Mon Sep 17 00:00:00 2001 From: kraysent Date: Sun, 8 Mar 2026 23:52:25 +0000 Subject: [PATCH 2/4] add hint --- src/components/core/Hint.tsx | 2 +- src/pages/Records.tsx | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/core/Hint.tsx b/src/components/core/Hint.tsx index 879182e..0932c48 100644 --- a/src/components/core/Hint.tsx +++ b/src/components/core/Hint.tsx @@ -19,7 +19,7 @@ export function Hint(props: HintProps): ReactElement { content={props.hintContent} arrow={false} placement="top" - className="bg-gray-600 z-10 backdrop-blur-sm bg-opacity-99 border-1 max-w-xl" + className="bg-gray-600 z-10 border-1 max-w-xl" > diff --git a/src/pages/Records.tsx b/src/pages/Records.tsx index 9724118..7beea15 100644 --- a/src/pages/Records.tsx +++ b/src/pages/Records.tsx @@ -129,9 +129,12 @@ function RecordsTable({ ); } + const nameHint = data?.schema?.catalogs?.designation?.description?.name; + const columns: Column[] = [ { name: "Name", + hint: nameHint ?

{nameHint}

: undefined, renderCell: (recordIndex: CellPrimitive) => { if (typeof recordIndex === "number" && data?.records[recordIndex]) { return getRecordName(data.records[recordIndex]); @@ -182,9 +185,9 @@ function RecordsTable({ const tableData: Record[] = data?.records.map((_record: RecordType, index: number) => { const row: Record = { - "Name": index, + Name: index, "Manual check status": index, - "Nature": index, + Nature: index, }; if (showCandidates) { row["Candidates"] = index; From 49178e161c2b1d519c1a64d63a77280678ff0494 Mon Sep 17 00:00:00 2001 From: kraysent Date: Sun, 8 Mar 2026 23:55:13 +0000 Subject: [PATCH 3/4] use hint in navbar --- src/components/core/Hint.tsx | 28 +++++++++++++++++++++++++--- src/components/ui/Navbar.tsx | 13 ++++++------- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/components/core/Hint.tsx b/src/components/core/Hint.tsx index 0932c48..9dcbb54 100644 --- a/src/components/core/Hint.tsx +++ b/src/components/core/Hint.tsx @@ -2,24 +2,46 @@ import { Tooltip } from "flowbite-react"; import { ReactElement, ReactNode } from "react"; import { MdHelpOutline } from "react-icons/md"; +export type HintPosition = "top" | "left" | "right" | "bottom"; + interface HintProps { children: ReactElement; hintContent: ReactNode; + position?: HintPosition; className?: string; + trigger?: "icon" | "child"; } +const tooltipClassName = "bg-gray-600 z-10 border-1 max-w-xl"; + export function Hint(props: HintProps): ReactElement { + const placement = props.position ?? "top"; + const trigger = props.trigger ?? "icon"; + + if (trigger === "child") { + return ( + + {props.children} + + ); + } + return (
{props.children}
diff --git a/src/components/ui/Navbar.tsx b/src/components/ui/Navbar.tsx index 1c451dd..3326f61 100644 --- a/src/components/ui/Navbar.tsx +++ b/src/components/ui/Navbar.tsx @@ -1,7 +1,7 @@ import { useEffect, useRef, useState } from "react"; import { NavLink } from "react-router-dom"; -import { Tooltip } from "flowbite-react"; import { MdInfo, MdSearch, MdTableChart } from "react-icons/md"; +import { Hint } from "../core/Hint"; import { Link } from "../core/Link"; const navItems = [ @@ -34,12 +34,11 @@ export function Navbar() { <>