From 72cab26d82e3a9dee05f1967e41b4ad9050292c3 Mon Sep 17 00:00:00 2001 From: kraysent Date: Sun, 19 Apr 2026 12:51:14 +0100 Subject: [PATCH 1/5] ability to change table meta for logged in users --- src/pages/TableDetails.tsx | 203 +++++++++++++++++++++++++++++++++++-- 1 file changed, 195 insertions(+), 8 deletions(-) diff --git a/src/pages/TableDetails.tsx b/src/pages/TableDetails.tsx index 28af69a..2e97ed9 100644 --- a/src/pages/TableDetails.tsx +++ b/src/pages/TableDetails.tsx @@ -1,11 +1,12 @@ -import { ReactElement } from "react"; +import { KeyboardEvent, ReactElement, useEffect, useState } from "react"; import { Bibliography, GetTableResponse, RecordCrossmatchStatus, } from "../clients/admin/types.gen"; -import { getTable } from "../clients/admin/sdk.gen"; +import { getTable, patchTable } from "../clients/admin/sdk.gen"; import { useNavigate, useParams } from "react-router-dom"; +import { MdEdit } from "react-icons/md"; import { CellPrimitive, Column, @@ -18,7 +19,8 @@ import { Link } from "../components/core/Link"; import { Loading } from "../components/core/Loading"; import { ErrorPage } from "../components/ui/ErrorPage"; import { useDataFetching } from "../hooks/useDataFetching"; -import { backendClient } from "../clients/config"; +import { adminClient } from "../clients/config"; +import { isLoggedIn } from "../auth/token"; function renderBibliography(bib: Bibliography): ReactElement { let authors = ""; @@ -82,9 +84,128 @@ function renderColumnName(name: CellPrimitive): ReactElement { interface TableMetaProps { tableName: string; table: GetTableResponse; + onAfterPatch: () => void; } function TableMeta(props: TableMetaProps): ReactElement { + const navigate = useNavigate(); + const canEdit = isLoggedIn(); + const [editingName, setEditingName] = useState(false); + const [editingDescription, setEditingDescription] = useState(false); + const showEditPencils = canEdit && !editingName && !editingDescription; + const [draftName, setDraftName] = useState(props.tableName); + const [draftDescription, setDraftDescription] = useState( + props.table.description, + ); + const [savingField, setSavingField] = useState<"name" | "description" | null>( + null, + ); + const [patchError, setPatchError] = useState(null); + + useEffect(() => { + if (!editingName) { + setDraftName(props.tableName); + } + }, [props.tableName, editingName]); + + useEffect(() => { + if (!editingDescription) { + setDraftDescription(props.table.description); + } + }, [props.table.description, editingDescription]); + + async function commitName(): Promise { + const trimmed = draftName.trim(); + if (!trimmed) { + setDraftName(props.tableName); + setEditingName(false); + setPatchError(null); + return; + } + if (trimmed === props.tableName) { + setEditingName(false); + setPatchError(null); + return; + } + setPatchError(null); + setSavingField("name"); + try { + const response = await patchTable({ + client: adminClient, + body: { + table_name: props.tableName, + new_table_name: trimmed, + }, + }); + if (response.error) { + throw new Error(JSON.stringify(response.error)); + } + setEditingName(false); + navigate(`/table/${encodeURIComponent(trimmed)}`); + } catch (err) { + setPatchError(`${err}`); + } finally { + setSavingField(null); + } + } + + async function commitDescription(): Promise { + const trimmed = draftDescription.trim(); + if (trimmed === props.table.description) { + setEditingDescription(false); + setPatchError(null); + return; + } + setPatchError(null); + setSavingField("description"); + try { + const response = await patchTable({ + client: adminClient, + body: { + table_name: props.tableName, + description: trimmed, + }, + }); + if (response.error) { + throw new Error(JSON.stringify(response.error)); + } + setEditingDescription(false); + props.onAfterPatch(); + } catch (err) { + setPatchError(`${err}`); + } finally { + setSavingField(null); + } + } + + function handleNameKeyDown(event: KeyboardEvent): void { + if (event.key === "Enter") { + event.preventDefault(); + void commitName(); + } + if (event.key === "Escape") { + event.preventDefault(); + setDraftName(props.tableName); + setEditingName(false); + setPatchError(null); + } + } + + function handleDescriptionKeyDown( + event: KeyboardEvent, + ): void { + if (event.key === "Enter") { + event.preventDefault(); + void commitDescription(); + } + if (event.key === "Escape") { + event.preventDefault(); + setDraftDescription(props.table.description); + setEditingDescription(false); + setPatchError(null); + } + } + const columns = [{ name: "Parameter" }, { name: "Value" }]; const values: Record[] = [ @@ -112,8 +233,69 @@ function TableMeta(props: TableMetaProps): ReactElement { return ( -

{props.table.description}

-

{props.tableName}

+
+ {editingDescription ? ( + setDraftDescription(event.target.value)} + onKeyDown={handleDescriptionKeyDown} + disabled={savingField === "description"} + className="text-2xl font-bold bg-transparent border border-gray-500 rounded px-2 py-0.5 flex-1 min-w-0 text-white" + autoFocus + /> + ) : ( +

+ {props.table.description} +

+ )} + {showEditPencils && ( + + )} +
+
+ {editingName ? ( + setDraftName(event.target.value)} + onKeyDown={handleNameKeyDown} + disabled={savingField === "name"} + className="text-gray-300 font-mono bg-transparent border border-gray-500 rounded px-2 py-0.5 flex-1 min-w-0" + autoFocus + /> + ) : ( +

+ {props.tableName} +

+ )} + {showEditPencils && ( + + )} +
+ {patchError ? ( +

{patchError}

+ ) : null}
); } @@ -231,7 +413,7 @@ async function fetcher( } const response = await getTable({ - client: backendClient, + client: adminClient, query: { table_name: tableName }, }); if (response.error) { @@ -244,12 +426,13 @@ async function fetcher( export function TableDetailsPage(): ReactElement { const { tableName } = useParams<{ tableName: string }>(); const navigate = useNavigate(); + const [refreshKey, setRefreshKey] = useState(0); const { data: payload, loading, error, - } = useDataFetching(() => fetcher(tableName), [tableName]); + } = useDataFetching(() => fetcher(tableName), [tableName, refreshKey]); function Content(): ReactElement { if (loading) return ; @@ -257,7 +440,11 @@ export function TableDetailsPage(): ReactElement { if (payload) { return ( <> - + setRefreshKey((key) => key + 1)} + /> Date: Sun, 19 Apr 2026 12:55:44 +0100 Subject: [PATCH 2/5] datatype changing --- src/pages/TableDetails.tsx | 71 +++++++++++++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 4 deletions(-) diff --git a/src/pages/TableDetails.tsx b/src/pages/TableDetails.tsx index 2e97ed9..0001d77 100644 --- a/src/pages/TableDetails.tsx +++ b/src/pages/TableDetails.tsx @@ -1,6 +1,7 @@ import { KeyboardEvent, ReactElement, useEffect, useState } from "react"; import { Bibliography, + DataType, GetTableResponse, RecordCrossmatchStatus, } from "../clients/admin/types.gen"; @@ -22,6 +23,25 @@ import { useDataFetching } from "../hooks/useDataFetching"; import { adminClient } from "../clients/config"; import { isLoggedIn } from "../auth/token"; +const DATA_TYPES: DataType[] = [ + "regular", + "reprocessing", + "preliminary", + "compilation", +]; + +function asDataType(value: unknown): DataType { + if ( + value === "regular" || + value === "reprocessing" || + value === "preliminary" || + value === "compilation" + ) { + return value; + } + return "regular"; +} + function renderBibliography(bib: Bibliography): ReactElement { let authors = ""; @@ -97,9 +117,9 @@ function TableMeta(props: TableMetaProps): ReactElement { const [draftDescription, setDraftDescription] = useState( props.table.description, ); - const [savingField, setSavingField] = useState<"name" | "description" | null>( - null, - ); + const [savingField, setSavingField] = useState< + "name" | "description" | "datatype" | null + >(null); const [patchError, setPatchError] = useState(null); useEffect(() => { @@ -206,8 +226,51 @@ function TableMeta(props: TableMetaProps): ReactElement { } } + async function commitDatatype(next: DataType): Promise { + const current = asDataType(props.table.meta.datatype); + if (next === current) { + return; + } + setPatchError(null); + setSavingField("datatype"); + try { + const response = await patchTable({ + client: adminClient, + body: { + table_name: props.tableName, + datatype: next, + }, + }); + if (response.error) { + throw new Error(JSON.stringify(response.error)); + } + props.onAfterPatch(); + } catch (err) { + setPatchError(`${err}`); + } finally { + setSavingField(null); + } + } + const columns = [{ name: "Parameter" }, { name: "Value" }]; + const datatypeValue: CellPrimitive = canEdit ? ( + + ) : ( + String(props.table.meta.datatype) + ); + const values: Record[] = [ { Parameter: "Table ID", @@ -223,7 +286,7 @@ function TableMeta(props: TableMetaProps): ReactElement { }, { Parameter: "Type of data", - Value: String(props.table.meta.datatype), + Value: datatypeValue, }, { Parameter: "Modification time", From 4412ef64833f7bea2f2febd090cab968a8c18c23 Mon Sep 17 00:00:00 2001 From: kraysent Date: Mon, 20 Apr 2026 21:26:16 +0100 Subject: [PATCH 3/5] fix crossmatch --- src/pages/TableDetails.tsx | 64 +++++++++++++++++++++++++------------- 1 file changed, 43 insertions(+), 21 deletions(-) diff --git a/src/pages/TableDetails.tsx b/src/pages/TableDetails.tsx index 0001d77..ffc4899 100644 --- a/src/pages/TableDetails.tsx +++ b/src/pages/TableDetails.tsx @@ -1,9 +1,10 @@ import { KeyboardEvent, ReactElement, useEffect, useState } from "react"; import { Bibliography, + CrossmatchTriageStatus, DataType, GetTableResponse, - RecordCrossmatchStatus, + TableCrossmatchResultStatus, } from "../clients/admin/types.gen"; import { getTable, patchTable } from "../clients/admin/sdk.gen"; import { useNavigate, useParams } from "react-router-dom"; @@ -15,7 +16,7 @@ import { } from "../components/ui/CommonTable"; import { Button } from "../components/core/Button"; import { CopyButton } from "../components/ui/CopyButton"; -import { Badge } from "../components/ui/Badge"; +import { Badge, BadgeType } from "../components/ui/Badge"; import { Link } from "../components/core/Link"; import { Loading } from "../components/core/Loading"; import { ErrorPage } from "../components/ui/ErrorPage"; @@ -374,28 +375,44 @@ function CrossmatchStats(props: CrossmatchStatsProps): ReactElement { const values: Record[] = []; - if (props.table.statistics) { - const statusLabels: Record = { - unprocessed: "Unprocessed", - new: "New", - collided: "Collided", - existing: "Existing", - }; - - Object.entries(props.table.statistics).forEach(([status, count]) => { - values.push({ - Status: statusLabels[status as RecordCrossmatchStatus] || status, - Count: count || 0, - }); - }); - } - - if (values.length === 0) { + if (!props.table.crossmatch) { return
; } + const triageLabels: Record = { + unprocessed: "Unprocessed", + pending: "Pending", + resolved: "Resolved", + }; + const resultLabels: Record = { + not_started: "Not started", + in_progress: "In progress", + done: "Done", + }; + const resultBadgeTypes: Record = { + not_started: "info", + in_progress: "warning", + done: "success", + }; + const triageOrder: CrossmatchTriageStatus[] = [ + "unprocessed", + "pending", + "resolved", + ]; + + triageOrder.forEach((status) => { + const count = props.table.crossmatch.statuses[status] ?? 0; + if (count <= 0) { + return; + } + values.push({ + Status: triageLabels[status], + Count: count, + }); + }); + function handleViewCrossmatchResults(event: React.MouseEvent): void { - const url = `/crossmatch?table_name=${encodeURIComponent(props.tableName)}&status=collided`; + const url = `/crossmatch?table_name=${encodeURIComponent(props.tableName)}&triage_status=pending`; if (event.ctrlKey || event.metaKey) { window.open(url, "_blank"); @@ -407,7 +424,12 @@ function CrossmatchStats(props: CrossmatchStatsProps): ReactElement { return (
-

Crossmatch Statistics

+
+

Crossmatch

+ + {resultLabels[props.table.crossmatch.result]} + +
From e430a99e400b09d790dbca0f7a5cb6e443409f0a Mon Sep 17 00:00:00 2001 From: kraysent Date: Tue, 21 Apr 2026 20:07:50 +0100 Subject: [PATCH 4/5] reduce duplication in commit functions --- src/pages/TableDetails.tsx | 109 ++++++++++++++++++------------------- 1 file changed, 54 insertions(+), 55 deletions(-) diff --git a/src/pages/TableDetails.tsx b/src/pages/TableDetails.tsx index ffc4899..2db42cf 100644 --- a/src/pages/TableDetails.tsx +++ b/src/pages/TableDetails.tsx @@ -123,6 +123,34 @@ function TableMeta(props: TableMetaProps): ReactElement { >(null); const [patchError, setPatchError] = useState(null); + async function runTablePatch( + field: "name" | "description" | "datatype", + body: { + table_name: string; + new_table_name?: string; + description?: string; + datatype?: DataType; + }, + onSuccess: () => void, + ): Promise { + setPatchError(null); + setSavingField(field); + try { + const response = await patchTable({ + client: adminClient, + body, + }); + if (response.error) { + throw new Error(JSON.stringify(response.error)); + } + onSuccess(); + } catch (err) { + setPatchError(`${err}`); + } finally { + setSavingField(null); + } + } + useEffect(() => { if (!editingName) { setDraftName(props.tableName); @@ -148,26 +176,17 @@ function TableMeta(props: TableMetaProps): ReactElement { setPatchError(null); return; } - setPatchError(null); - setSavingField("name"); - try { - const response = await patchTable({ - client: adminClient, - body: { - table_name: props.tableName, - new_table_name: trimmed, - }, - }); - if (response.error) { - throw new Error(JSON.stringify(response.error)); - } + await runTablePatch( + "name", + { + table_name: props.tableName, + new_table_name: trimmed, + }, + () => { setEditingName(false); navigate(`/table/${encodeURIComponent(trimmed)}`); - } catch (err) { - setPatchError(`${err}`); - } finally { - setSavingField(null); - } + }, + ); } async function commitDescription(): Promise { @@ -177,26 +196,17 @@ function TableMeta(props: TableMetaProps): ReactElement { setPatchError(null); return; } - setPatchError(null); - setSavingField("description"); - try { - const response = await patchTable({ - client: adminClient, - body: { - table_name: props.tableName, - description: trimmed, - }, - }); - if (response.error) { - throw new Error(JSON.stringify(response.error)); - } + await runTablePatch( + "description", + { + table_name: props.tableName, + description: trimmed, + }, + () => { setEditingDescription(false); props.onAfterPatch(); - } catch (err) { - setPatchError(`${err}`); - } finally { - setSavingField(null); - } + }, + ); } function handleNameKeyDown(event: KeyboardEvent): void { @@ -232,25 +242,14 @@ function TableMeta(props: TableMetaProps): ReactElement { if (next === current) { return; } - setPatchError(null); - setSavingField("datatype"); - try { - const response = await patchTable({ - client: adminClient, - body: { - table_name: props.tableName, - datatype: next, - }, - }); - if (response.error) { - throw new Error(JSON.stringify(response.error)); - } - props.onAfterPatch(); - } catch (err) { - setPatchError(`${err}`); - } finally { - setSavingField(null); - } + await runTablePatch( + "datatype", + { + table_name: props.tableName, + datatype: next, + }, + () => props.onAfterPatch(), + ); } const columns = [{ name: "Parameter" }, { name: "Value" }]; From 62e4384fc3d4dea38454c519d982cb59b8bf2d9a Mon Sep 17 00:00:00 2001 From: kraysent Date: Tue, 21 Apr 2026 20:09:27 +0100 Subject: [PATCH 5/5] make fix --- src/pages/TableDetails.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/TableDetails.tsx b/src/pages/TableDetails.tsx index 2db42cf..83f93f4 100644 --- a/src/pages/TableDetails.tsx +++ b/src/pages/TableDetails.tsx @@ -183,8 +183,8 @@ function TableMeta(props: TableMetaProps): ReactElement { new_table_name: trimmed, }, () => { - setEditingName(false); - navigate(`/table/${encodeURIComponent(trimmed)}`); + setEditingName(false); + navigate(`/table/${encodeURIComponent(trimmed)}`); }, ); } @@ -203,8 +203,8 @@ function TableMeta(props: TableMetaProps): ReactElement { description: trimmed, }, () => { - setEditingDescription(false); - props.onAfterPatch(); + setEditingDescription(false); + props.onAfterPatch(); }, ); }