From 75e1b7f895f3572b7db3dae6ceacea777a716ca0 Mon Sep 17 00:00:00 2001 From: kraysent Date: Sun, 24 May 2026 23:11:38 +0100 Subject: [PATCH] rewrite data-catalog page to use tap endpoints --- src/pages/DataCatalog.tsx | 272 +++++++++++++++++++++++++------------- 1 file changed, 180 insertions(+), 92 deletions(-) diff --git a/src/pages/DataCatalog.tsx b/src/pages/DataCatalog.tsx index c00087e..4ae87d4 100644 --- a/src/pages/DataCatalog.tsx +++ b/src/pages/DataCatalog.tsx @@ -1,14 +1,16 @@ import { ReactElement, useEffect, useMemo, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; -import { listSchemas, getTable } from "../clients/backend/sdk.gen"; +import { tapSync, tapTables } from "../clients/backend/sdk.gen"; import type { - ColumnInfo, - GetTableResponse, - ListSchemasResponse, - SchemaEntry, + ListTapTablesResponse, + TapColumnInfo, + TapSchemaEntry, + TapSyncResponse, + TapTableInfo, ValidationError, } from "../clients/backend/types.gen"; import { backendClient } from "../clients/config"; +import { isLoggedIn } from "../auth/token"; import { useDataFetching } from "../hooks/useDataFetching"; import { Loading } from "../components/core/Loading"; import { ErrorPage } from "../components/ui/ErrorPage"; @@ -30,24 +32,26 @@ function formatApiError(error: unknown): string { return JSON.stringify(error); } -async function fetchSchemaList(): Promise { - const response = await listSchemas({ client: backendClient }); +async function fetchTablesList(): Promise { + const response = await tapTables({ + client: backendClient, + query: { detail: "max" }, + }); if (response.error) { throw new Error(formatApiError(response.error)); } if (!response.data?.data) { - throw new Error("No schema data received from server"); + throw new Error("No table list received from server"); } return response.data.data; } -async function fetchTablePreview( - schemaName: string, - tableName: string, -): Promise { - const response = await getTable({ +async function fetchTableRows(tableName: string): Promise { + const response = await tapSync({ client: backendClient, - query: { schema_name: schemaName, table_name: tableName }, + query: { + query: `SELECT * FROM ${tableName}`, + }, }); if (response.error) { throw new Error(formatApiError(response.error)); @@ -58,10 +62,19 @@ async function fetchTablePreview( return response.data.data; } +function findTableInfo( + schemas: TapSchemaEntry[] | undefined, + schemaName: string, + tableName: string, +): TapTableInfo | null { + const schema = schemas?.find((s) => s.schema_name === schemaName); + return schema?.tables.find((t) => t.name === tableName) ?? null; +} + function filterSchemas( - schemas: SchemaEntry[] | undefined, + schemas: TapSchemaEntry[] | undefined, query: string, -): SchemaEntry[] { +): TapSchemaEntry[] { if (!schemas?.length) { return []; } @@ -74,15 +87,25 @@ function filterSchemas( ...s, tables: s.tables.filter((t) => { const blob = - `${s.schema_name} ${t.table_name} ${s.description ?? ""} ${t.description ?? ""}`.toLowerCase(); + `${s.schema_name} ${t.name} ${t.description ?? ""}`.toLowerCase(); return blob.includes(needle); }), })) .filter((s) => s.tables.length > 0); } +function cellValue(value: unknown): CellPrimitive { + if (value === null || value === undefined) { + return "—"; + } + if (typeof value === "number") { + return value; + } + return String(value); +} + interface SchemaSidebarProps { - schemas: SchemaEntry[]; + schemas: TapSchemaEntry[]; selectedSchema: string | null; selectedTable: string | null; onSelect: (schemaName: string, tableName: string) => void; @@ -99,20 +122,19 @@ function SchemaSidebar({ {schemas.map((schema) => (
    {schema.tables.map((t) => { const active = selectedSchema === schema.schema_name && - selectedTable === t.table_name; + selectedTable === t.name; return ( -
  • +
  • @@ -149,18 +171,14 @@ function SchemaSidebar({ ); } -interface TablePreviewProps { - payload: GetTableResponse; -} - -function columnMetadataHint(column: ColumnInfo): ReactElement { +function columnMetadataHint(column: TapColumnInfo): ReactElement { return (
    {column.description ? {column.description} : null}
    Type - {column.data_type ?? "—"} + {column.datatype ?? "—"} Unit @@ -175,44 +193,107 @@ function columnMetadataHint(column: ColumnInfo): ReactElement { ); } -function TablePreview({ payload }: TablePreviewProps): ReactElement { - const sampleColumnDefs: Column[] = payload.columns.map((c) => ({ - name: c.column_name, +const catalogPanelClassName = + "rounded-lg border border-dashed border-border p-8 text-center"; + +function CatalogBrowsePrompt(): ReactElement { + return ( +
    + + Browse the data + + + Choose a table on the left to see column definitions and sample rows + +
    + ); +} + +function CatalogLoginPrompt(): ReactElement { + return ( +
    + + Log in to view data in tables + + + Sign in to load rows after you choose a table on the left + +
    + ); +} + +interface TableDetailProps { + tableInfo: TapTableInfo; + syncPayload: TapSyncResponse | null; + syncLoading: boolean; + syncError: string | null; + loggedIn: boolean; +} + +function TableDetail({ + tableInfo, + syncPayload, + syncLoading, + syncError, + loggedIn, +}: TableDetailProps): ReactElement { + const metadataColumns = tableInfo.columns ?? []; + const syncTable = syncPayload?.resource.table; + const syncColumns = syncTable?.columns ?? []; + + const columnsForHints: TapColumnInfo[] = metadataColumns.length + ? metadataColumns + : syncColumns.map((c) => ({ + name: c.name, + datatype: c.datatype, + unit: c.unit ?? null, + })); + + const columnDefs: Column[] = columnsForHints.map((c) => ({ + name: c.name, hint: columnMetadataHint(c), })); - const sampleRows: Record[] = payload.sample_rows.map( - (row) => { - const out: Record = {}; - for (const col of payload.columns) { - const v = row[col.column_name]; - out[col.column_name] = v ?? "—"; - } - return out; - }, - ); + const rows: Record[] = loggedIn + ? (syncTable?.data ?? []).map((row) => { + const out: Record = {}; + for (let i = 0; i < syncColumns.length; i++) { + out[syncColumns[i].name] = cellValue(row[i]); + } + return out; + }) + : []; return (
    - {payload.description ?? ( + {tableInfo.description ?? ( - {payload.schema_name}.{payload.table_name} + {tableInfo.name} )} - {payload.description ? ( + {tableInfo.description ? ( - {payload.schema_name}.{payload.table_name} + {tableInfo.name} ) : null}
    - - - Sample rows - - + + {!loggedIn ? ( + + ) : syncError ? ( + + ) : syncLoading ? ( + + ) : ( + + + Sample rows + + + )}
    ); } @@ -224,6 +305,7 @@ export function DataCatalogPage(): ReactElement { }>(); const navigate = useNavigate(); const [filter, setFilter] = useState(""); + const loggedIn = isLoggedIn(); const selectedSchema = schemaName ?? null; const selectedTable = tableName ?? null; @@ -233,27 +315,34 @@ export function DataCatalogPage(): ReactElement { }, []); const { - data: schemaPayload, - loading: schemasLoading, - error: schemasError, - } = useDataFetching(() => fetchSchemaList(), []); + data: tablesPayload, + loading: tablesLoading, + error: tablesError, + } = useDataFetching(() => fetchTablesList(), []); const { - data: tablePayload, - loading: tableLoading, - error: tableError, - } = useDataFetching((): Promise => { - if (!selectedSchema || !selectedTable) { + data: syncPayload, + loading: syncLoading, + error: syncError, + } = useDataFetching((): Promise => { + if (!selectedSchema || !selectedTable || !loggedIn) { return Promise.resolve(null); } - return fetchTablePreview(selectedSchema, selectedTable); - }, [selectedSchema ?? "", selectedTable ?? ""]); + return fetchTableRows(selectedTable); + }, [selectedTable ?? "", loggedIn]); const filtered = useMemo( - () => filterSchemas(schemaPayload?.schemas, filter), - [schemaPayload?.schemas, filter], + () => filterSchemas(tablesPayload?.schemas, filter), + [tablesPayload?.schemas, filter], ); + const selectedTableInfo = useMemo(() => { + if (!selectedSchema || !selectedTable) { + return null; + } + return findTableInfo(tablesPayload?.schemas, selectedSchema, selectedTable); + }, [tablesPayload?.schemas, selectedSchema, selectedTable]); + function handleSelect(nextSchema: string, nextTable: string): void { navigate( `/data-catalog/${encodeURIComponent(nextSchema)}/${encodeURIComponent(nextTable)}`, @@ -261,18 +350,18 @@ export function DataCatalogPage(): ReactElement { } function SidebarContent(): ReactElement { - if (schemasError && !schemaPayload) { - return ; + if (tablesError && !tablesPayload) { + return ; } - if (schemasLoading && !schemaPayload) { + if (tablesLoading && !tablesPayload) { return ; } if (!filtered.length) { return ( - {schemaPayload?.schemas.length + {tablesPayload?.schemas.length ? "No tables match your filter." - : "No schemas returned by the API."} + : "No tables returned by the API."} ); } @@ -288,36 +377,35 @@ export function DataCatalogPage(): ReactElement { function DetailContent(): ReactElement { if (!selectedSchema || !selectedTable) { - return ( -
    - - Browse the data - - - Choose a table on the left to see column definitions and sample rows - -
    - ); + return loggedIn ? : ; } - const previewMatchesSelection = - tablePayload && - tablePayload.schema_name === selectedSchema && - tablePayload.table_name === selectedTable; - - if (tableError) { - return ; + if (tablesError && !tablesPayload) { + return ; } - if (tableLoading && !previewMatchesSelection) { + if (tablesLoading && !selectedTableInfo) { return ; } - if (tablePayload && previewMatchesSelection) { - return ; + if (!selectedTableInfo) { + return ( + + ); } - return ; + return ( + + ); } return (