diff --git a/routes/sources.js b/routes/sources.js index 326dc3e..bda2a63 100644 --- a/routes/sources.js +++ b/routes/sources.js @@ -225,10 +225,26 @@ module.exports = function(pool) { return res.status(400).json({ error: `"${col}" is not a key column` }); } + // ?q= narrows, ?limit= caps. A key column can be very wide -- part on + // osm_skinny has 11,290 distinct values -- so returning the lot to fill a + // completion list is both a slow query and a large response for a control + // that can only usefully show a handful. const { schema, tname } = srcResult.rows[0]; + const q = (req.query.q || '').trim(); + const limit = Math.min(parseInt(req.query.limit) || 5000, 5000); + + const params = []; + let filter = `WHERE "${col}" IS NOT NULL`; + if (q) { + params.push(`%${q}%`); + filter += ` AND "${col}"::text ILIKE $${params.length}`; + } + params.push(limit); + const result = await pool.query( - `SELECT DISTINCT "${col}" AS val FROM ${schema}.${tname} - WHERE "${col}" IS NOT NULL ORDER BY "${col}"` + `SELECT DISTINCT "${col}"::text AS val FROM ${schema}.${tname} + ${filter} ORDER BY 1 LIMIT $${params.length}`, + params ); res.json(result.rows.map(r => r.val)); } catch (err) { diff --git a/ui/src/components/OperationPanel.jsx b/ui/src/components/OperationPanel.jsx index 70f0478..2a58b2a 100644 --- a/ui/src/components/OperationPanel.jsx +++ b/ui/src/components/OperationPanel.jsx @@ -11,7 +11,7 @@ // flex-1 buttons grew to absurd sizes; everything here is fixed-width and // left-aligned instead. -import { useState } from 'react' +import { useState, useEffect } from 'react' const INPUT = 'border border-gray-200 rounded px-2 py-1 text-xs bg-white w-28 text-right font-mono tabular-nums' const TEXT = 'border border-gray-200 rounded px-2 py-1 text-xs bg-white w-40 font-mono' @@ -538,9 +538,49 @@ function ScaleLedger({ currentTotals, scaleInputs, setScaleInputs, scalePlug, se ) } +// Completion for a key dimension. The list is fetched as you type rather than up +// front: part alone has 11,290 distinct values, and a native datalist given all of +// them is slow to open and no easier to read than a short filtered one. +function DimValueInput({ col, sourceId, value, onChange, onBlur, className }) { + const [options, setOptions] = useState([]) + const listId = `pf-vals-${col.cname}` + + useEffect(() => { + if (!sourceId || !col.is_key) return + let cancelled = false + // wait for a pause in typing; every keystroke would otherwise be a query + const t = setTimeout(async () => { + try { + const url = `/api/sources/${sourceId}/values/${encodeURIComponent(col.cname)}` + + `?limit=50${value ? `&q=${encodeURIComponent(value)}` : ''}` + const rows = await fetch(url).then(r => r.ok ? r.json() : []) + if (!cancelled) setOptions(Array.isArray(rows) ? rows : []) + } catch { if (!cancelled) setOptions([]) } + }, 200) + return () => { cancelled = true; clearTimeout(t) } + }, [sourceId, col.cname, col.is_key, value]) + + return ( + <> + + {col.is_key && ( + + {options.map(o => + )} + + ) +} + // ── 2b. Recode / clone form ───────────────────────────────────────────────── // Same pairing: the dimension's current value sits beside the box that replaces it. -function DimForm({ dimCols, setObj, setSet, slices, lookupDerivedCols, extra }) { +function DimForm({ dimCols, setObj, setSet, slices, lookupDerivedCols, sourceId, extra }) { const multi = slices.length > 1 const first = slices[0] || {} return ( @@ -563,13 +603,14 @@ function DimForm({ dimCols, setObj, setSet, slices, lookupDerivedCols, extra }) {c.label || c.cname} {cur} - setSet(s => ({ ...s, [c.cname]: e.target.value }))} onBlur={c.is_key && c.dim_group ? e => lookupDerivedCols(c.cname, e.target.value, setSet) : undefined} - placeholder="keep" className={TEXT} /> @@ -661,7 +702,7 @@ export default function OperationPanel({ cloneSet, setCloneSet, cloneScale, setCloneScale, cloneNote, setCloneNote, - dimCols, lookupDerivedCols, + dimCols, lookupDerivedCols, sourceId, buildPayload, submitOp, }) { const hasSlice = slices.length > 0 @@ -722,12 +763,12 @@ export default function OperationPanel({ )} {activeOp === 'recode' && ( } /> )} {activeOp === 'clone' && (
diff --git a/ui/src/views/Forecast.jsx b/ui/src/views/Forecast.jsx index ae7b87f..e1c86e2 100644 --- a/ui/src/views/Forecast.jsx +++ b/ui/src/views/Forecast.jsx @@ -1257,7 +1257,7 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio cloneSet, setCloneSet, cloneScale, setCloneScale, cloneNote, setCloneNote, - dimCols, lookupDerivedCols, + dimCols, lookupDerivedCols, sourceId, buildPayload, submitOp, }