From 9c33d66fa37fb9f5eaaf0e9aadbd07097c85c810 Mon Sep 17 00:00:00 2001 From: kraysent Date: Sun, 14 Jun 2026 18:55:48 +0200 Subject: [PATCH 1/3] ability to search columns in /table page --- src/pages/TableDetails.tsx | 59 ++++++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 15 deletions(-) diff --git a/src/pages/TableDetails.tsx b/src/pages/TableDetails.tsx index 699483d..21427b0 100644 --- a/src/pages/TableDetails.tsx +++ b/src/pages/TableDetails.tsx @@ -17,6 +17,7 @@ import { import { CopyButton } from "../components/ui/CopyButton"; import { Badge } from "../components/ui/Badge"; import { Link } from "../components/core/Link"; +import { TextFilter } from "../components/core/TextFilter"; import { Loading } from "../components/core/Loading"; import { Card, CardAction, Field } from "../components/ui/Card"; import { ErrorPage } from "../components/ui/ErrorPage"; @@ -518,7 +519,25 @@ interface ColumnInfoProps { table: GetTableResponse; } +function columnMatchesSearch( + col: GetTableResponse["column_info"][number], + query: string, +): boolean { + const needle = query.trim().toLowerCase(); + if (!needle) { + return true; + } + + const fields = [col.name, col.description, col.ucd]; + return fields.some( + (value) => + typeof value === "string" && value.toLowerCase().includes(needle), + ); +} + function ColumnInfo(props: ColumnInfoProps): ReactElement { + const [query, setQuery] = useState(""); + const columns: Column[] = [ { name: "Name", renderCell: renderColumnName }, { name: "Description" }, @@ -541,26 +560,36 @@ function ColumnInfo(props: ColumnInfoProps): ReactElement { const values: Record[] = []; - props.table.column_info.forEach((col) => { - const colValue: Record = { - Name: col.name, - }; + props.table.column_info + .filter((col) => columnMatchesSearch(col, query)) + .forEach((col) => { + const colValue: Record = { + Name: col.name, + }; - if (col.description) { - colValue.Description = col.description; - } - if (col.unit) { - colValue.Unit = col.unit; - } - if (col.ucd) { - colValue.UCD = col.ucd; - } + if (col.description) { + colValue.Description = col.description; + } + if (col.unit) { + colValue.Unit = col.unit; + } + if (col.ucd) { + colValue.UCD = col.ucd; + } - values.push(colValue); - }); + values.push(colValue); + }); return ( +
+ +
); From e11ee66f382b00abd7cadcc594cbc19ec2b94185 Mon Sep 17 00:00:00 2001 From: kraysent Date: Sun, 14 Jun 2026 19:05:39 +0200 Subject: [PATCH 2/3] add link to table query --- src/pages/TableDetails.tsx | 44 +++++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/src/pages/TableDetails.tsx b/src/pages/TableDetails.tsx index 21427b0..00dcf92 100644 --- a/src/pages/TableDetails.tsx +++ b/src/pages/TableDetails.tsx @@ -25,6 +25,7 @@ import { Hint } from "../components/ui/Hint"; import { useDataFetching } from "../hooks/useDataFetching"; import { adminClient } from "../clients/config"; import { isLoggedIn } from "../auth/token"; +import { originalDataCatalogLink } from "../components/catalogs/catalogActions"; const DATA_TYPES: DataType[] = [ "regular", @@ -45,6 +46,26 @@ function asDataType(value: unknown): DataType { return "regular"; } +function quoteSqlIdentifier(identifier: string): string { + return `"${identifier.replace(/"/g, '""')}"`; +} + +function selectAllColumnsFromRawdataTable( + tableName: string, + columns: GetTableResponse["column_info"], + limit = 25, +): string { + const qualifiedTable = `rawdata.${quoteSqlIdentifier(tableName)}`; + if (columns.length === 0) { + return `SELECT * FROM ${qualifiedTable} LIMIT ${limit}`; + } + + const columnList = columns + .map((column) => quoteSqlIdentifier(column.name)) + .join(", "); + return `SELECT ${columnList} FROM ${qualifiedTable} LIMIT ${limit}`; +} + function renderBibliography(bib: Bibliography): ReactElement { const targetLink = `https://ui.adsabs.harvard.edu/abs/${bib.bibcode}/abstract`; @@ -516,6 +537,7 @@ function CatalogProgressCard({ } interface ColumnInfoProps { + tableName: string; table: GetTableResponse; } @@ -536,8 +558,24 @@ function columnMatchesSearch( } function ColumnInfo(props: ColumnInfoProps): ReactElement { + const navigate = useNavigate(); const [query, setQuery] = useState(""); + const actions: CardAction[] = [ + { + title: "View table data", + onClick: () => + navigate( + originalDataCatalogLink( + selectAllColumnsFromRawdataTable( + props.tableName, + props.table.column_info, + ), + ), + ), + }, + ]; + const columns: Column[] = [ { name: "Name", renderCell: renderColumnName }, { name: "Description" }, @@ -581,13 +619,13 @@ function ColumnInfo(props: ColumnInfoProps): ReactElement { }); return ( - +
@@ -653,7 +691,7 @@ export function TableDetailsPage(): ReactElement { /> ) : null} - + ); } From d79bcb00cbf5e969c0d4b695ae2d98528389b7dc Mon Sep 17 00:00:00 2001 From: kraysent Date: Sun, 14 Jun 2026 19:34:48 +0200 Subject: [PATCH 3/3] only select checked columns --- src/components/core/TextFilter.tsx | 10 +-- src/pages/TableDetails.tsx | 108 +++++++++++++++++++++++------ 2 files changed, 92 insertions(+), 26 deletions(-) diff --git a/src/components/core/TextFilter.tsx b/src/components/core/TextFilter.tsx index 3c1979d..af6fce2 100644 --- a/src/components/core/TextFilter.tsx +++ b/src/components/core/TextFilter.tsx @@ -1,7 +1,7 @@ import { ReactElement } from "react"; interface TextFieldProps { - title: string; + title?: string; value: string; onChange: (value: string) => void; placeholder?: string; @@ -17,9 +17,11 @@ export function TextFilter({ }: TextFieldProps): ReactElement { return (
- + {title ? ( + + ) : null} onChange(e.target.value)} diff --git a/src/pages/TableDetails.tsx b/src/pages/TableDetails.tsx index 00dcf92..f3d5f8d 100644 --- a/src/pages/TableDetails.tsx +++ b/src/pages/TableDetails.tsx @@ -1,5 +1,11 @@ import classNames from "classnames"; -import { KeyboardEvent, ReactElement, useEffect, useState } from "react"; +import { + KeyboardEvent, + ReactElement, + useEffect, + useMemo, + useState, +} from "react"; import { Bibliography, DataType, @@ -541,6 +547,8 @@ interface ColumnInfoProps { table: GetTableResponse; } +const COLUMN_SELECT_KEY = ""; + function columnMatchesSearch( col: GetTableResponse["column_info"][number], query: string, @@ -560,6 +568,38 @@ function columnMatchesSearch( function ColumnInfo(props: ColumnInfoProps): ReactElement { const navigate = useNavigate(); const [query, setQuery] = useState(""); + const [selectedColumns, setSelectedColumns] = useState>( + () => new Set(), + ); + + useEffect(() => { + setSelectedColumns(new Set()); + }, [props.tableName]); + + useEffect(() => { + setSelectedColumns((prev) => { + const names = new Set(props.table.column_info.map((col) => col.name)); + return new Set([...prev].filter((name) => names.has(name))); + }); + }, [props.table.column_info]); + + function toggleColumn(name: string): void { + setSelectedColumns((prev) => { + const next = new Set(prev); + if (next.has(name)) { + next.delete(name); + } else { + next.add(name); + } + return next; + }); + } + + const selectedColumnInfo = useMemo( + () => + props.table.column_info.filter((col) => selectedColumns.has(col.name)), + [props.table.column_info, selectedColumns], + ); const actions: CardAction[] = [ { @@ -569,32 +609,56 @@ function ColumnInfo(props: ColumnInfoProps): ReactElement { originalDataCatalogLink( selectAllColumnsFromRawdataTable( props.tableName, - props.table.column_info, + selectedColumnInfo, ), ), ), }, ]; - const columns: Column[] = [ - { name: "Name", renderCell: renderColumnName }, - { name: "Description" }, - { name: "Unit" }, - { - name: "UCD", - renderCell: renderUCD, - hint: ( -

- Unified Content Descriptor. Describes astronomical quantities in a - structured way. For more information see{" "} - - IVOA Recommendation - - . -

- ), - }, - ]; + const columns: Column[] = useMemo( + () => [ + { + name: COLUMN_SELECT_KEY, + renderCell: (value: CellPrimitive) => { + const columnName = String(value); + return ( +
+ toggleColumn(columnName)} + onClick={(event) => event.stopPropagation()} + aria-label={`Select column ${columnName}`} + className="size-4 accent-accent cursor-pointer" + /> +
+ ); + }, + }, + { name: "Name", renderCell: renderColumnName }, + { name: "Description" }, + { name: "Unit" }, + { + name: "UCD", + renderCell: renderUCD, + hint: ( +

+ Unified Content Descriptor. Describes astronomical quantities in a + structured way. For more information see{" "} + + IVOA Recommendation + + . +

+ ), + }, + ], + [selectedColumns], + ); const values: Record[] = []; @@ -602,6 +666,7 @@ function ColumnInfo(props: ColumnInfoProps): ReactElement { .filter((col) => columnMatchesSearch(col, query)) .forEach((col) => { const colValue: Record = { + [COLUMN_SELECT_KEY]: col.name, Name: col.name, }; @@ -622,7 +687,6 @@ function ColumnInfo(props: ColumnInfoProps): ReactElement {