Add per-source default Perspective layout
Forecast falls back to a saved per-source layout when no version-local layout is cached, so new versions of a source open with a sensible pivot without each user reconfiguring it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
953ae2709f
commit
39335bca75
@ -222,6 +222,24 @@ module.exports = function(pool) {
|
||||
}
|
||||
});
|
||||
|
||||
// set or clear the default Perspective layout for a source.
|
||||
// Body: a Perspective view config (group_by, split_by, columns, plugin_config, …).
|
||||
// Pass null or {} to clear.
|
||||
router.put('/sources/:id/default-layout', async (req, res) => {
|
||||
try {
|
||||
const layout = req.body && Object.keys(req.body).length > 0 ? req.body : null;
|
||||
const result = await pool.query(
|
||||
`UPDATE pf.source SET default_layout = $1 WHERE id = $2 RETURNING *`,
|
||||
[layout, req.params.id]
|
||||
);
|
||||
if (result.rows.length === 0) return res.status(404).json({ error: 'Source not found' });
|
||||
res.json(result.rows[0]);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// deregister a source — does not drop existing forecast tables
|
||||
router.delete('/sources/:id', async (req, res) => {
|
||||
try {
|
||||
|
||||
@ -9,11 +9,15 @@ CREATE TABLE IF NOT EXISTS pf.source (
|
||||
tname text NOT NULL,
|
||||
label text,
|
||||
status text NOT NULL DEFAULT 'active', -- active | archived
|
||||
default_layout jsonb, -- Perspective view config used as the per-source default
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
created_by text,
|
||||
UNIQUE (schema, tname)
|
||||
);
|
||||
|
||||
-- backfill column for existing installs
|
||||
ALTER TABLE pf.source ADD COLUMN IF NOT EXISTS default_layout jsonb;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS pf.col_meta (
|
||||
id serial PRIMARY KEY,
|
||||
source_id integer NOT NULL REFERENCES pf.source(id) ON DELETE CASCADE,
|
||||
|
||||
@ -29,7 +29,7 @@ function cleanLayout(cfg, validCols) {
|
||||
return c
|
||||
}
|
||||
|
||||
export default function Forecast({ sourceId, versionId }) {
|
||||
export default function Forecast({ sources = [], sourceId, versionId, refreshSources }) {
|
||||
const { dark } = useTheme()
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [largeDataset, setLargeDataset] = useState(false)
|
||||
@ -237,15 +237,23 @@ export default function Forecast({ sourceId, versionId }) {
|
||||
cfg.plugin_config = { edit_mode: 'SELECT_REGION', ...(cfg.plugin_config || {}) }
|
||||
await viewer.restore(cfg)
|
||||
if (cfg.expand_depth != null) await applyDepth(cfg.expand_depth)
|
||||
} else {
|
||||
const sourceDefault = sources.find(s => String(s.id) === String(sid))?.default_layout
|
||||
let cfg
|
||||
if (sourceDefault && Object.keys(sourceDefault).length > 0) {
|
||||
cfg = cleanLayout(sourceDefault, validCols)
|
||||
cfg.table = tableName
|
||||
cfg.plugin_config = { edit_mode: 'SELECT_REGION', ...(cfg.plugin_config || {}) }
|
||||
} else {
|
||||
const valueCol = meta.find(c => c.role === 'value')?.cname
|
||||
const cfg = {
|
||||
cfg = {
|
||||
table: tableName,
|
||||
settings: false,
|
||||
group_by: ['pf_iter'],
|
||||
columns: valueCol ? [valueCol] : [],
|
||||
plugin_config: { edit_mode: 'SELECT_REGION' }
|
||||
}
|
||||
}
|
||||
await viewer.restore(cfg)
|
||||
}
|
||||
|
||||
@ -320,6 +328,22 @@ export default function Forecast({ sourceId, versionId }) {
|
||||
flash('Saved')
|
||||
}
|
||||
|
||||
async function saveAsSourceDefault() {
|
||||
const cfg = await captureConfig()
|
||||
if (!cfg) return
|
||||
const { table, expand_depth, ...rest } = cfg
|
||||
try {
|
||||
const res = await fetch(`/api/sources/${sourceId}/default-layout`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(rest)
|
||||
})
|
||||
if (!res.ok) { const data = await res.json(); flash(data.error || 'Failed', 'error'); return }
|
||||
if (refreshSources) await refreshSources()
|
||||
flash('Saved as source default')
|
||||
} catch (err) { flash(err.message, 'error') }
|
||||
}
|
||||
|
||||
async function handleSaveOver() {
|
||||
const layout = layouts.find(l => l.id === activeLayoutId)
|
||||
if (!layout) return
|
||||
@ -492,6 +516,11 @@ export default function Forecast({ sourceId, versionId }) {
|
||||
<button onClick={() => setShowSaveAs(true)} className="border border-dashed border-gray-200 text-gray-400 hover:text-gray-600 rounded px-2 py-0.5">
|
||||
Save as…
|
||||
</button>
|
||||
<button onClick={saveAsSourceDefault} disabled={!sourceId}
|
||||
className="border border-dashed border-gray-200 text-gray-400 hover:text-gray-600 rounded px-2 py-0.5 disabled:opacity-40"
|
||||
title="Use this layout as the default for new versions of this source">
|
||||
Set source default
|
||||
</button>
|
||||
{activeLayoutId !== null && (
|
||||
<button onClick={resetLayout} className="text-gray-300 hover:text-red-400">Reset</button>
|
||||
)}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user