void;
placeholder?: string;
- type?: "text" | "email" | "password" | "number";
onEnter?: () => void;
}
@@ -14,7 +13,6 @@ export function TextFilter({
value,
onChange,
placeholder,
- type = "text",
onEnter,
}: TextFieldProps): ReactElement {
return (
@@ -23,7 +21,6 @@ export function TextFilter({
{title}
onChange(e.target.value)}
onKeyDown={(e) => {
diff --git a/src/hooks/useDataFetching.ts b/src/hooks/useDataFetching.ts
new file mode 100644
index 0000000..d4fe15f
--- /dev/null
+++ b/src/hooks/useDataFetching.ts
@@ -0,0 +1,33 @@
+import { useEffect, useState } from "react";
+
+interface UseDataFetchingResult
{
+ data: T | null;
+ loading: boolean;
+ error: string | null;
+}
+
+export function useDataFetching(
+ fetcher: () => Promise,
+ dependencies: React.DependencyList = [],
+): UseDataFetchingResult {
+ const [data, setData] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ async function fetchData(): Promise {
+ try {
+ const result = await fetcher();
+ setData(result);
+ } catch (err) {
+ setError(`${err}`);
+ } finally {
+ setLoading(false);
+ }
+ }
+
+ fetchData();
+ }, dependencies);
+
+ return { data, loading, error };
+}
diff --git a/src/pages/CrossmatchResults.tsx b/src/pages/CrossmatchResults.tsx
index 0c8b6b3..97e2c11 100644
--- a/src/pages/CrossmatchResults.tsx
+++ b/src/pages/CrossmatchResults.tsx
@@ -1,5 +1,5 @@
import { ReactElement, useEffect, useState } from "react";
-import { useSearchParams, useNavigate } from "react-router-dom";
+import { useSearchParams } from "react-router-dom";
import {
CommonTable,
Column,
@@ -13,28 +13,28 @@ import type {
GetRecordsCrossmatchResponse,
RecordCrossmatch,
RecordCrossmatchStatus,
- HttpValidationError,
ValidationError,
} from "../clients/admin/types.gen";
import { getResource } from "../resources/resources";
import { Button } from "../components/ui/button";
import { Loading } from "../components/ui/loading";
-import { ErrorPage, ErrorPageHomeButton } from "../components/ui/error-page";
+import { ErrorPage } from "../components/ui/error-page";
import { Link } from "../components/ui/link";
+import { useDataFetching } from "../hooks/useDataFetching";
-export function CrossmatchResultsPage(): ReactElement {
- const [searchParams, setSearchParams] = useSearchParams();
- const navigate = useNavigate();
-
- const [data, setData] = useState(null);
- const [error, setError] = useState(null);
- const [loading, setLoading] = useState(true);
-
- const tableName = searchParams.get("table_name");
- const status = searchParams.get("status") as RecordCrossmatchStatus | null;
- const page = parseInt(searchParams.get("page") || "0");
- const pageSize = parseInt(searchParams.get("page_size") || "25");
+interface CrossmatchFiltersProps {
+ tableName: string | null;
+ status: RecordCrossmatchStatus | null;
+ pageSize: number;
+ onApplyFilters: (tableName: string, status: string, pageSize: number) => void;
+}
+function CrossmatchFilters({
+ tableName,
+ status,
+ pageSize,
+ onApplyFilters,
+}: CrossmatchFiltersProps): ReactElement {
const [localStatus, setLocalStatus] = useState(status || "all");
const [localPageSize, setLocalPageSize] = useState(pageSize);
const [localTableName, setLocalTableName] = useState(tableName || "");
@@ -45,79 +45,55 @@ export function CrossmatchResultsPage(): ReactElement {
setLocalTableName(tableName || "");
}, [status, pageSize, tableName]);
- useEffect(() => {
- async function fetchData() {
- if (!tableName) {
- navigate("/");
- return;
- }
-
- try {
- setLoading(true);
- const response =
- await getCrossmatchRecordsAdminApiV1RecordsCrossmatchGet({
- query: {
- table_name: tableName,
- status: status || undefined,
- page: page,
- page_size: pageSize,
- },
- });
-
- if (response.error) {
- setError(response.error);
- return;
- }
-
- if (response.data) {
- setData(response.data.data);
- }
- } catch (err) {
- console.error("Error fetching crossmatch records", err);
- setError({
- detail: [
- {
- loc: [],
- msg: "Failed to fetch crossmatch records",
- type: "value_error",
- },
- ],
- });
- } finally {
- setLoading(false);
- }
- }
-
- fetchData();
- }, [tableName, status, page, pageSize, navigate]);
-
- function handlePageChange(newPage: number): void {
- const newSearchParams = new URLSearchParams(searchParams);
- newSearchParams.set("page", newPage.toString());
- setSearchParams(newSearchParams);
- }
-
function applyFilters(): void {
- const newSearchParams = new URLSearchParams(searchParams);
-
- if (localTableName.trim()) {
- newSearchParams.set("table_name", localTableName.trim());
- } else {
- newSearchParams.delete("table_name");
- }
-
- if (localStatus === "all") {
- newSearchParams.delete("status");
- } else {
- newSearchParams.set("status", localStatus);
- }
+ onApplyFilters(localTableName, localStatus, localPageSize);
+ }
- newSearchParams.set("page_size", localPageSize.toString());
- newSearchParams.set("page", "0");
+ return (
+
+
+
+
+
setLocalPageSize(parseInt(value))}
+ />
+
+
+
+
+ );
+}
- setSearchParams(newSearchParams);
- }
+interface CrossmatchResultsProps {
+ data: GetRecordsCrossmatchResponse | null;
+}
+function CrossmatchResults({ data }: CrossmatchResultsProps): ReactElement {
function getRecordName(record: RecordCrossmatch): ReactElement {
const displayName = record.catalogs.designation?.name || record.record_id;
return (
@@ -128,30 +104,23 @@ export function CrossmatchResultsPage(): ReactElement {
}
function renderCandidates(record: RecordCrossmatch): ReactElement {
- if (record.status === "new") {
- return NULL;
- }
+ let pgcNumbers: number[] = [];
if (record.status === "existing" && record.metadata.pgc) {
- const pgcText = `${record.metadata.pgc}`;
- return {pgcText};
- }
-
- if (record.status === "collided" && record.metadata.possible_matches) {
- const pgcNumbers = record.metadata.possible_matches;
-
- return (
-
- {pgcNumbers.map((pgc: number, index: number) => (
-
- {pgc}
-
- ))}
-
- );
+ pgcNumbers = [record.metadata.pgc];
+ } else if (record.status === "collided") {
+ pgcNumbers = record.metadata.possible_matches ?? [];
}
- return NULL;
+ return (
+ <>
+ {pgcNumbers.map((pgc, index) => (
+
+ {pgc}
+
+ ))}
+ >
+ );
}
function getStatusLabel(status: RecordCrossmatchStatus): string {
@@ -160,7 +129,7 @@ export function CrossmatchResultsPage(): ReactElement {
const columns: Column[] = [
{
- name: "Record Name",
+ name: "Record name",
renderCell: (recordIndex: CellPrimitive) => {
if (typeof recordIndex === "number" && data?.records[recordIndex]) {
return getRecordName(data.records[recordIndex]);
@@ -182,111 +151,134 @@ export function CrossmatchResultsPage(): ReactElement {
const tableData: Record[] =
data?.records.map((record: RecordCrossmatch, index: number) => ({
- "Record Name": index,
+ "Record name": index,
Status: getStatusLabel(record.status),
Candidates: index,
})) || [];
- if (loading) {
- return ;
+ return ;
+}
+
+async function fetcher(
+ tableName: string | null,
+ status: RecordCrossmatchStatus | null,
+ page: number,
+ pageSize: number,
+): Promise {
+ if (!tableName) {
+ throw new Error("Table name is required");
}
- if (error) {
- return (
- err.msg).join(", ") ||
- "An error occurred"
- }
- className="p-8"
- >
- navigate("/")} />
-
+ const response = await getCrossmatchRecordsAdminApiV1RecordsCrossmatchGet({
+ query: {
+ table_name: tableName,
+ status: status,
+ page: page,
+ page_size: pageSize,
+ },
+ });
+
+ if (response.error) {
+ throw new Error(
+ response.error.detail
+ ?.map((err: ValidationError) => err.msg)
+ .join(", ") || "Failed to fetch crossmatch records",
);
}
- if (!tableName) {
+ if (!response.data) {
+ throw new Error("No data received from server");
+ }
+
+ return response.data.data;
+}
+
+export function CrossmatchResultsPage(): ReactElement {
+ const [searchParams, setSearchParams] = useSearchParams();
+
+ const tableName = searchParams.get("table_name");
+ const status = searchParams.get("status") as RecordCrossmatchStatus | null;
+ const page = parseInt(searchParams.get("page") || "0");
+ const pageSize = parseInt(searchParams.get("page_size") || "25");
+
+ useEffect(() => {
+ document.title = `Crossmatch - ${tableName} | HyperLEDA`;
+ }, [tableName]);
+
+ const { data, loading, error } = useDataFetching(
+ () => fetcher(tableName, status, page, pageSize),
+ [tableName, status, page, pageSize],
+ );
+
+ function handlePageChange(newPage: number): void {
+ const newSearchParams = new URLSearchParams(searchParams);
+ newSearchParams.set("page", newPage.toString());
+ setSearchParams(newSearchParams);
+ }
+
+ function handleApplyFilters(
+ newTableName: string,
+ newStatus: string,
+ newPageSize: number,
+ ): void {
+ const newSearchParams = new URLSearchParams(searchParams);
+
+ if (newTableName.trim()) {
+ newSearchParams.set("table_name", newTableName.trim());
+ } else {
+ newSearchParams.delete("table_name");
+ }
+
+ if (newStatus === "all") {
+ newSearchParams.delete("status");
+ } else {
+ newSearchParams.set("status", newStatus);
+ }
+
+ newSearchParams.set("page_size", newPageSize.toString());
+ newSearchParams.set("page", "0");
+
+ setSearchParams(newSearchParams);
+ }
+
+ function Content(): ReactElement {
+ if (loading) return ;
+ if (error) return ;
+
return (
-
- navigate("/")} />
-
+ <>
+
+
+
+
+ Page {page + 1} (showing {data?.records.length} records)
+
+
+
+ >
);
}
return (
-
-
-
Crossmatch results
-
-
-
-
-
-
setLocalPageSize(parseInt(value))}
- />
-
-
-
-
-
-
-
-
-
Crossmatch records
-
- Showing {tableData.length} records
-
-
-
-
-
-
- Page {page + 1}
-
-
-
+ <>
+ Crossmatch results
+
+
+ >
);
}
diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx
index ea8a759..d251bd7 100644
--- a/src/pages/Home.tsx
+++ b/src/pages/Home.tsx
@@ -1,5 +1,3 @@
-import { NavigateFunction, useNavigate } from "react-router-dom";
-import { SearchBar } from "../components/ui/searchbar";
import { ReactElement } from "react";
import { Link } from "../components/ui/link";
@@ -35,21 +33,12 @@ const homePageHint: ReactElement = (
);
-function searchHandler(navigate: NavigateFunction) {
- return function f(query: string) {
- navigate(`/query?q=${encodeURIComponent(query)}`);
- };
-}
-
export function HomePage(): ReactElement {
- const navigate = useNavigate();
-
return (
-