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 699483d..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, @@ -17,6 +23,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"; @@ -24,6 +31,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", @@ -44,6 +52,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`; @@ -515,52 +543,155 @@ function CatalogProgressCard({ } interface ColumnInfoProps { + tableName: string; table: GetTableResponse; } +const COLUMN_SELECT_KEY = ""; + +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 columns: Column[] = [ - { name: "Name", renderCell: renderColumnName }, - { name: "Description" }, - { name: "Unit" }, + 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[] = [ { - name: "UCD", - renderCell: renderUCD, - hint: ( -

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

- ), + title: "View table data", + onClick: () => + navigate( + originalDataCatalogLink( + selectAllColumnsFromRawdataTable( + props.tableName, + selectedColumnInfo, + ), + ), + ), }, ]; + 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[] = []; - 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 = { + [COLUMN_SELECT_KEY]: col.name, + 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 ( - + +
+ +
); @@ -624,7 +755,7 @@ export function TableDetailsPage(): ReactElement { /> ) : null}
- + ); }