Compare commits
2 Commits
master
...
feature/pe
| Author | SHA1 | Date | |
|---|---|---|---|
| b1eb68a475 | |||
| 4b9296abc1 |
37
CLAUDE.md
37
CLAUDE.md
@ -95,6 +95,43 @@ Turning a region back into slices re-derives, per cell, the same filters Perspec
|
||||
|
||||
---
|
||||
|
||||
## Column hierarchy (collapse / expand)
|
||||
|
||||
The two pivot axes collapse by completely different mechanisms, and the asymmetry is a
|
||||
Perspective constraint, not a choice:
|
||||
|
||||
- **Rows.** The `GROUP BY ROLLUP` view holds every level at once; `view.set_depth()` — which
|
||||
lives on the view, not the config — hides the deeper ones. That is what the `EXPAND 0 1 2 3`
|
||||
buttons drive, via `applyDepth()`.
|
||||
- **Columns.** There is no equivalent. `expand()` / `collapse()` take a **row index**,
|
||||
`ViewConfig` has `group_by_depth` but no `split_by_depth`, and `split_rollup_mode`
|
||||
(`'flat' | 'rollup'`) only chooses whether subtotal column groups are *emitted* — it is a
|
||||
view shape, not an interaction. So `applySplitDepth(n)` collapses by restoring a
|
||||
**truncated `split_by`**, which rebuilds the view.
|
||||
|
||||
Three things follow from the rebuild, and each is handled:
|
||||
|
||||
1. The full hierarchy has to be remembered separately — once collapsed, `viewer.save()`
|
||||
only reports the short `split_by`. `splitFullRef` / `splitFull` hold it, and it is
|
||||
persisted into the saved layout as `split_full` so a reload while collapsed can still
|
||||
expand back. `adoptSplit()` is the single place it is set.
|
||||
2. `perspective-config-update` fires for our own restore as well as the user rearranging
|
||||
the pivot. `collapsingRef` distinguishes them — without it, a collapse would overwrite
|
||||
the full hierarchy with the truncated one and the deeper levels would be unreachable.
|
||||
3. Row depth lives on the discarded view, so `applyDepth(expandDepthRef.current)` is
|
||||
re-applied afterwards — the same wart as the refocus re-apply.
|
||||
|
||||
The selection is cleared on every change: slices name the split_by dimensions they were
|
||||
cut from, and the highlight is keyed on grid coordinates. Neither survives a column axis
|
||||
that just changed shape.
|
||||
|
||||
**Limitation:** this is whole-axis, not per-branch. Excel can collapse 2025 while 2026
|
||||
stays expanded; truncating `split_by` collapses every column group at that level together.
|
||||
Per-branch is not reachable — `columns` selects which *measures* appear, not individual
|
||||
split combinations.
|
||||
|
||||
---
|
||||
|
||||
## Operation SQL patterns
|
||||
|
||||
All three operations follow the same structure: insert a `pf.log` row in a CTE, then insert forecast rows referencing its id. `{{where_clause}}` is built from the slice; `{{exclude_clause}}` blocks `exclude_iters` rows.
|
||||
|
||||
48
ui/package-lock.json
generated
48
ui/package-lock.json
generated
@ -8,10 +8,10 @@
|
||||
"name": "ui",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@perspective-dev/client": "5.2.0",
|
||||
"@perspective-dev/server": "5.2.0",
|
||||
"@perspective-dev/viewer": "5.2.0",
|
||||
"@perspective-dev/viewer-datagrid": "5.2.0",
|
||||
"@perspective-dev/client": "file:./vendor/perspective-dev-client-5.4.0.tgz",
|
||||
"@perspective-dev/server": "file:./vendor/perspective-dev-server-5.4.0.tgz",
|
||||
"@perspective-dev/viewer": "file:./vendor/perspective-dev-viewer-5.4.0.tgz",
|
||||
"@perspective-dev/viewer-datagrid": "file:./vendor/perspective-dev-viewer-datagrid-5.4.0.tgz",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5"
|
||||
},
|
||||
@ -554,43 +554,43 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@perspective-dev/client": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@perspective-dev/client/-/client-5.2.0.tgz",
|
||||
"integrity": "sha512-zkJmJFwdw0wMREoJt8gUJyCsflzi7s7Yc8ZtUCOLE9XB6fdyxJmDB99cxO3Q+hno/57kLsz8VNoz8XSm6PZNrg==",
|
||||
"version": "5.4.0",
|
||||
"resolved": "file:vendor/perspective-dev-client-5.4.0.tgz",
|
||||
"integrity": "sha512-l9xCJ0W42wm9fLlwhoU838KyTE131qQTS686uQb/VcsLs30kCVjkoermCCUs0YHCDwDTJhly1y9DfAZDbkg9Ng==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@perspective-dev/server": "",
|
||||
"@perspective-dev/server": "^5.4.0",
|
||||
"pro_self_extracting_wasm": "0.0.9",
|
||||
"stoppable": "=1.1.0",
|
||||
"ws": "^8.17.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@perspective-dev/server": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@perspective-dev/server/-/server-5.2.0.tgz",
|
||||
"integrity": "sha512-WRBiokT2/BYM8Ipe7Dg1/2UYNb01Vsox79KBfFVI800VmwdId0GdqI95zufN5ZoFffeBfri1sr3S3AShBRFXKA==",
|
||||
"version": "5.4.0",
|
||||
"resolved": "file:vendor/perspective-dev-server-5.4.0.tgz",
|
||||
"integrity": "sha512-iTseRJB6TL6D9xjaMKMhh2NEKMIi9JR881J+GyQflHIQXK43fDlsIWtByUAoyZzZ7uA9KNZJZicirvONxIpLuw==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@perspective-dev/viewer": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@perspective-dev/viewer/-/viewer-5.2.0.tgz",
|
||||
"integrity": "sha512-IIyqdPduofzzZ/QWrteq29IadJQR8IapTRz7U6XbrtBBm7eyiFsIBKQV0wFfcqWg4TRnhuvCAUvBRieuE0R8Eg==",
|
||||
"version": "5.4.0",
|
||||
"resolved": "file:vendor/perspective-dev-viewer-5.4.0.tgz",
|
||||
"integrity": "sha512-7D6jNn7tDZ3W84MsydplWAJbqqpXIz5OlsHx5YG4sHvJ5q8ngZQvP+oZdxLQXL4hIbaxpskMld1gJbGltpcvUw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@perspective-dev/client": "",
|
||||
"@perspective-dev/client": "^5.4.0",
|
||||
"pro_self_extracting_wasm": "0.0.9",
|
||||
"regular-layout": "=0.6.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@perspective-dev/viewer-datagrid": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@perspective-dev/viewer-datagrid/-/viewer-datagrid-5.2.0.tgz",
|
||||
"integrity": "sha512-v/SR/35YfyKfivO21nglJFiyG1iE5G8vMwIwWI6fQuwjW764pmNrm/zcrNWNY70x5XHkPRM94aY6LjkLsLkO2g==",
|
||||
"version": "5.4.0",
|
||||
"resolved": "file:vendor/perspective-dev-viewer-datagrid-5.4.0.tgz",
|
||||
"integrity": "sha512-7ITOmrIh1ZpiQbUR/KmHYck1sLA4cpNgpVMI/qJjQc9k/uypkT1vJmKYxv4MKR24TmEkOouBLNYiO+ItFKs8vg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@perspective-dev/client": "",
|
||||
"@perspective-dev/viewer": "",
|
||||
"regular-table": "=0.8.6"
|
||||
"@perspective-dev/client": "^5.4.0",
|
||||
"@perspective-dev/viewer": "^5.4.0",
|
||||
"regular-table": "=0.9.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@rolldown/binding-android-arm64": {
|
||||
@ -2948,9 +2948,9 @@
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/regular-table": {
|
||||
"version": "0.8.6",
|
||||
"resolved": "https://registry.npmjs.org/regular-table/-/regular-table-0.8.6.tgz",
|
||||
"integrity": "sha512-jzJzu9WLtqwMgbf5ak/VdNm/YLHUDnDSCHD5SOksnHrHLuDN6l5i2stYfe7BjsHbQV1Gx9369Ma40nTBCgOFvA==",
|
||||
"version": "0.9.1",
|
||||
"resolved": "https://registry.npmjs.org/regular-table/-/regular-table-0.9.1.tgz",
|
||||
"integrity": "sha512-3I/2I3NEhmyScCevW/r1AE9hyUkqrPRMDsgo/nDnGXhpXVZOxUddJQGBmEMLgyc7OSaQSzl9BtXllnMqw1MwCA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
|
||||
@ -10,10 +10,10 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@perspective-dev/client": "5.2.0",
|
||||
"@perspective-dev/server": "5.2.0",
|
||||
"@perspective-dev/viewer": "5.2.0",
|
||||
"@perspective-dev/viewer-datagrid": "5.2.0",
|
||||
"@perspective-dev/client": "file:./vendor/perspective-dev-client-5.4.0.tgz",
|
||||
"@perspective-dev/server": "file:./vendor/perspective-dev-server-5.4.0.tgz",
|
||||
"@perspective-dev/viewer": "file:./vendor/perspective-dev-viewer-5.4.0.tgz",
|
||||
"@perspective-dev/viewer-datagrid": "file:./vendor/perspective-dev-viewer-datagrid-5.4.0.tgz",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5"
|
||||
},
|
||||
|
||||
@ -23,6 +23,8 @@ function cleanLayout(cfg, validCols) {
|
||||
if (c.columns) c.columns = c.columns.filter(col => col == null || ok(col))
|
||||
if (c.group_by) c.group_by = c.group_by.filter(ok)
|
||||
if (c.split_by) c.split_by = c.split_by.filter(ok)
|
||||
// the uncollapsed column hierarchy travels with the layout (see applySplitDepth)
|
||||
if (c.split_full) c.split_full = c.split_full.filter(ok)
|
||||
if (c.sort) c.sort = c.sort.filter(([col]) => ok(col))
|
||||
if (c.filter) c.filter = c.filter.filter(([col]) => ok(col))
|
||||
return c
|
||||
@ -42,6 +44,12 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
|
||||
const [saveAsName, setSaveAsName] = useState('')
|
||||
|
||||
// operation panel — a selection is a LIST of slices; one entry is the common case
|
||||
// The column hierarchy and how many of its levels are showing. Mirrored into
|
||||
// splitFullRef for handlers registered once; held as state so the toolbar
|
||||
// re-renders when either changes.
|
||||
const [splitFull, setSplitFull] = useState([])
|
||||
const [splitDepth, setSplitDepth] = useState(null)
|
||||
|
||||
const [slices, setSlices] = useState([])
|
||||
const [applyMode, setApplyMode] = useState('prorate') // 'prorate' | 'each'
|
||||
const [activeOp, setActiveOp] = useState('scale')
|
||||
@ -167,6 +175,12 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
|
||||
const tableRef = useRef(null)
|
||||
const colMetaRef = useRef([])
|
||||
const expandDepthRef = useRef(null)
|
||||
// The column axis has no set_depth() — collapsing it means restoring a shorter
|
||||
// split_by, so the full hierarchy has to be remembered separately to expand again.
|
||||
const splitFullRef = useRef([])
|
||||
// set while our own restore is in flight, so the config-update listener can tell
|
||||
// a collapse from the user rearranging split_by themselves
|
||||
const collapsingRef = useRef(false)
|
||||
const initIdRef = useRef(0)
|
||||
const modifierRef = useRef(false)
|
||||
// the datagrid plugin element, for reading cell coordinates and driving its
|
||||
@ -536,6 +550,7 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
|
||||
setLoadProgress(null)
|
||||
setSlices([])
|
||||
expandDepthRef.current = null
|
||||
adoptSplit([], 0)
|
||||
try {
|
||||
const [dataResult, meta] = await Promise.all([
|
||||
fetch(`/api/versions/${vid}/data`).then(async r => {
|
||||
@ -616,6 +631,9 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
|
||||
const { table: _t, ...rest } = cleanLayout(JSON.parse(saved), validCols)
|
||||
const cfg = { ...rest, plugin_config: { ...(rest.plugin_config || {}), edit_mode: 'SELECT_REGION' } }
|
||||
await viewer.restore(cfg)
|
||||
// split_full outlives split_by: a layout saved while collapsed still knows
|
||||
// the levels it was collapsed from
|
||||
adoptSplit(cfg.split_full?.length ? cfg.split_full : cfg.split_by, (cfg.split_by || []).length)
|
||||
if (cfg.expand_depth != null) await applyDepth(cfg.expand_depth)
|
||||
} else {
|
||||
const sourceDefault = sources.find(s => String(s.id) === String(sid))?.default_layout
|
||||
@ -633,12 +651,19 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
|
||||
}
|
||||
}
|
||||
await viewer.restore(cfg)
|
||||
adoptSplit(cfg.split_full?.length ? cfg.split_full : cfg.split_by, (cfg.split_by || []).length)
|
||||
}
|
||||
|
||||
// auto-persist viewer state (formatting, columns, etc.) to the last-used cache
|
||||
if (viewer._pspUpdate) viewer.removeEventListener('perspective-config-update', viewer._pspUpdate)
|
||||
viewer._pspUpdate = async () => {
|
||||
try {
|
||||
// A split_by change that is not ours is the user rearranging the pivot, and
|
||||
// it redefines the hierarchy. Ours is a collapse, and must not overwrite it.
|
||||
if (!collapsingRef.current) {
|
||||
const live = await viewer.save()
|
||||
adoptSplit(live.split_by || [], (live.split_by || []).length)
|
||||
}
|
||||
const cfg = await captureConfig()
|
||||
if (cfg) await persistLayout(vid, cfg)
|
||||
} catch {}
|
||||
@ -690,6 +715,54 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
|
||||
}
|
||||
}
|
||||
|
||||
// Record the column hierarchy as the uncollapsed truth. Called whenever a layout
|
||||
// arrives or the user rearranges split_by themselves — but never for our own
|
||||
// collapse, which would otherwise overwrite the full list with the short one.
|
||||
function adoptSplit(full, depth) {
|
||||
const list = Array.isArray(full) ? full : []
|
||||
splitFullRef.current = list
|
||||
setSplitFull(list)
|
||||
setSplitDepth(depth == null ? list.length : Math.min(depth, list.length))
|
||||
}
|
||||
|
||||
// Collapse or expand the column axis to `n` split_by levels.
|
||||
//
|
||||
// The row axis gets this for free: its GROUP BY ROLLUP view holds every level at
|
||||
// once and view.set_depth() hides the deeper ones. The column axis has no
|
||||
// equivalent — there is no split_by_depth in ViewConfig and expand()/collapse()
|
||||
// take a row index — so collapsing means restoring a truncated split_by, which
|
||||
// rebuilds the view. Two consequences fall out of that: the row depth has to be
|
||||
// re-applied afterwards (it lives on the discarded view), and it is whole-axis,
|
||||
// not per-branch — every column group collapses to the same level together.
|
||||
async function applySplitDepth(n) {
|
||||
const viewer = viewerRef.current
|
||||
const full = splitFullRef.current
|
||||
if (!viewer || !full.length) return
|
||||
const depth = Math.max(0, Math.min(n, full.length))
|
||||
collapsingRef.current = true
|
||||
try {
|
||||
await viewer.restore({ split_by: full.slice(0, depth) })
|
||||
setSplitDepth(depth)
|
||||
// restore() rebuilt the view, so the row depth that lived on the old one is gone
|
||||
if (expandDepthRef.current != null) await applyDepth(expandDepthRef.current)
|
||||
} catch (err) {
|
||||
console.error('[applySplitDepth]', err)
|
||||
flash(err.message || String(err), 'error')
|
||||
return
|
||||
} finally {
|
||||
collapsingRef.current = false
|
||||
}
|
||||
// a slice names the split_by dimensions it was cut from, and the highlight is
|
||||
// keyed on grid coordinates — neither survives a column axis that just changed
|
||||
setSlices([])
|
||||
try {
|
||||
const cfg = await captureConfig()
|
||||
if (cfg) await persistLayout(versionId, cfg)
|
||||
} catch (err) {
|
||||
console.error('[applySplitDepth persist]', err)
|
||||
}
|
||||
}
|
||||
|
||||
async function applyDepth(d) {
|
||||
const viewer = viewerRef.current
|
||||
if (!viewer) return
|
||||
@ -704,7 +777,7 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
|
||||
const viewer = viewerRef.current
|
||||
if (!viewer) return null
|
||||
const cfg = await viewer.save()
|
||||
return { ...cfg, expand_depth: expandDepthRef.current }
|
||||
return { ...cfg, expand_depth: expandDepthRef.current, split_full: splitFullRef.current }
|
||||
}
|
||||
|
||||
async function persistLayout(vid, cfg) {
|
||||
@ -762,6 +835,7 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
|
||||
const cfg = cleanLayout(layout.config, validCols)
|
||||
cfg.plugin_config = { ...(cfg.plugin_config || {}), edit_mode: 'SELECT_REGION' }
|
||||
await viewer.restore(cfg)
|
||||
adoptSplit(cfg.split_full?.length ? cfg.split_full : cfg.split_by, (cfg.split_by || []).length)
|
||||
if (cfg.expand_depth != null) await applyDepth(cfg.expand_depth)
|
||||
setActiveLayoutId(layout.id)
|
||||
await persistLayout(versionId, cfg)
|
||||
@ -1087,6 +1161,30 @@ export default function Forecast({ sources = [], sourceId, versions = [], versio
|
||||
))}
|
||||
</div>
|
||||
|
||||
{splitFull.length > 0 && (
|
||||
<>
|
||||
<div className="w-px h-4 bg-gray-200 shrink-0" />
|
||||
|
||||
{/* Column hierarchy group — the split_by equivalent of Expand */}
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="text-gray-400 uppercase tracking-wide" style={{fontSize:'10px'}}>Columns</span>
|
||||
{Array.from({ length: splitFull.length + 1 }, (_, n) => {
|
||||
const label = n === 0 ? 'Total' : splitFull[n - 1]
|
||||
return (
|
||||
<button key={n} onClick={() => applySplitDepth(n)}
|
||||
title={n === 0
|
||||
? 'Collapse the columns to a single total'
|
||||
: `Show columns down to ${splitFull.slice(0, n).join(' › ')}`}
|
||||
className={`border rounded px-1.5 py-0.5 transition-colors max-w-[9rem] truncate
|
||||
${splitDepth === n ? 'border-blue-300 text-blue-600 bg-blue-50' : 'border-gray-200 text-gray-500 hover:border-gray-400'}`}>
|
||||
{label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="w-px h-4 bg-gray-200 shrink-0" />
|
||||
|
||||
{/* Data group */}
|
||||
|
||||
7
ui/vendor/PROVENANCE.txt
vendored
Normal file
7
ui/vendor/PROVENANCE.txt
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
Built from https://github.com/fleetside72/perspective
|
||||
branch column-axis-expand-collapse
|
||||
commit 2e3901d652650a33eaf19c2ddf049f7e525ea95b
|
||||
based on v5.4.0
|
||||
built 2026-09-14T02:40:11Z on r710.hptrow.me
|
||||
|
||||
Regenerate with ui/vendor/rebuild-perspective.sh
|
||||
50
ui/vendor/README.md
vendored
Normal file
50
ui/vendor/README.md
vendored
Normal file
@ -0,0 +1,50 @@
|
||||
# Vendored Perspective
|
||||
|
||||
pf_app runs a **patched build of Perspective**. Upstream's C++ engine has always
|
||||
implemented column-axis expand/collapse — `t_ctx2::set_depth(HEADER_COLUMN, …)`
|
||||
and `open`/`close(HEADER_COLUMN, idx)` are fully written — but nothing above C++
|
||||
could reach it: `set_column_pivot_depth()` was never called, and
|
||||
`View<t_ctx2>::expand/collapse` hardcoded `HEADER_ROW`. The patch is wiring, not
|
||||
new engine logic.
|
||||
|
||||
It buys two things the released packages cannot do at all:
|
||||
|
||||
- `split_by_depth` in `ViewConfig`, the `split_by` counterpart to `group_by_depth`
|
||||
- `expand_column()` / `collapse_column()`, so one column branch can fold to its
|
||||
subtotal while its siblings stay expanded — the Excel behaviour
|
||||
|
||||
**Source:** https://github.com/fleetside72/perspective, branch
|
||||
`column-axis-expand-collapse`. See `PROVENANCE.txt` for the exact commit these
|
||||
tarballs were built from.
|
||||
|
||||
## Why tarballs and not npm
|
||||
|
||||
The feature is not released upstream. Until it is, the four packages are built
|
||||
from the fork and committed here as npm tarballs. `npm install` expands them
|
||||
exactly as it expands anything from the registry — no special tooling, and
|
||||
`pf.sh deploy` works unchanged. A deploy machine needs node and nothing else:
|
||||
no emscripten, no cmake, no protoc, no Rust.
|
||||
|
||||
All four move together, never a subset. Perspective couples loader, package
|
||||
versions, data format and `apache-arrow`; vendoring a partial set reintroduces
|
||||
exactly the drift that causes trouble.
|
||||
|
||||
## Changing the engine
|
||||
|
||||
./rebuild-perspective.sh # builds the fork, repacks, rewrites PROVENANCE.txt
|
||||
cd .. && npm install
|
||||
git add vendor && git commit
|
||||
|
||||
Push the fork first — the script warns if the source tree is dirty, because a
|
||||
tarball built from uncommitted code has no recoverable source.
|
||||
|
||||
The build itself needs cmake >= 3.29.5, protoc >= 22 (its version silently
|
||||
selects which protobuf source tree gets cloned), pnpm, and the Rust nightly the
|
||||
repo pins. Roughly 40 minutes cold. Only ever on a machine changing the engine.
|
||||
|
||||
## Getting rid of this
|
||||
|
||||
This is a fork, with the maintenance that implies. The exit is upstream taking
|
||||
the change — the patch is small and additive, and the engine work is already
|
||||
theirs. When a release ships it, delete this directory and put normal version
|
||||
ranges back in `ui/package.json`.
|
||||
BIN
ui/vendor/perspective-dev-client-5.4.0.tgz
vendored
Normal file
BIN
ui/vendor/perspective-dev-client-5.4.0.tgz
vendored
Normal file
Binary file not shown.
BIN
ui/vendor/perspective-dev-server-5.4.0.tgz
vendored
Normal file
BIN
ui/vendor/perspective-dev-server-5.4.0.tgz
vendored
Normal file
Binary file not shown.
BIN
ui/vendor/perspective-dev-viewer-5.4.0.tgz
vendored
Normal file
BIN
ui/vendor/perspective-dev-viewer-5.4.0.tgz
vendored
Normal file
Binary file not shown.
BIN
ui/vendor/perspective-dev-viewer-datagrid-5.4.0.tgz
vendored
Normal file
BIN
ui/vendor/perspective-dev-viewer-datagrid-5.4.0.tgz
vendored
Normal file
Binary file not shown.
102
ui/vendor/rebuild-perspective.sh
vendored
Executable file
102
ui/vendor/rebuild-perspective.sh
vendored
Executable file
@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# rebuild-perspective.sh — rebuild the patched Perspective and re-vendor it
|
||||
#
|
||||
# pf_app runs a patched build of Perspective that exposes the column axis
|
||||
# expand/collapse the engine already implements (split_by_depth, and
|
||||
# expand_column/collapse_column). Upstream does not ship this yet, so the
|
||||
# built packages are vendored into this directory as npm tarballs.
|
||||
#
|
||||
# Source of truth: https://github.com/fleetside72/perspective
|
||||
# branch column-axis-expand-collapse
|
||||
#
|
||||
# This script exists because vendored binaries are opaque: once the .tgz files
|
||||
# are committed, nothing in the repo records how to regenerate them. Run this
|
||||
# after changing the fork, then commit the resulting tarballs.
|
||||
#
|
||||
# Only needed on a machine that is changing the engine. Deploys just run
|
||||
# `npm install`, which expands the committed tarballs - see ../README in this
|
||||
# directory.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
PSP="${PSP_DIR:-$HOME/perspective}"
|
||||
BRANCH="${PSP_BRANCH:-column-axis-expand-collapse}"
|
||||
VENDOR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# The four packages that must move together. Perspective's own docs are
|
||||
# emphatic that loader, packages, data format and apache-arrow are one unit;
|
||||
# vendoring a subset would reintroduce exactly the drift that causes trouble.
|
||||
PACKAGES=(
|
||||
"rust/perspective-js"
|
||||
"rust/perspective-server"
|
||||
"rust/perspective-viewer"
|
||||
"packages/viewer-datagrid"
|
||||
)
|
||||
|
||||
info() { echo -e "\033[0;34m==>\033[0m $*"; }
|
||||
ok() { echo -e "\033[0;32m ✓\033[0m $*"; }
|
||||
die() { echo -e "\033[0;31m ✗\033[0m $*" >&2; exit 1; }
|
||||
|
||||
# -- preflight --------------------------------------------------------------
|
||||
[[ -d "$PSP" ]] || die "No Perspective checkout at $PSP.
|
||||
git clone https://github.com/fleetside72/perspective.git $PSP
|
||||
cd $PSP && git checkout $BRANCH
|
||||
Set PSP_DIR to use a different path."
|
||||
|
||||
command -v pnpm >/dev/null || die "pnpm not found. Perspective builds with pnpm, not npm."
|
||||
command -v protoc >/dev/null || die "protoc not found.
|
||||
Its VERSION selects which protobuf source tree the build clones, and a
|
||||
version below 22 pulls a layout the build cannot consume. Needs >= 22
|
||||
(33.2 known good). Distro packages are usually far too old."
|
||||
|
||||
cmake_ver=$(cmake --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+(\.[0-9]+)?') || die "cmake not found"
|
||||
cmake_major=${cmake_ver%%.*}; cmake_minor=$(echo "$cmake_ver" | cut -d. -f2)
|
||||
if (( cmake_major < 3 || (cmake_major == 3 && cmake_minor < 29) )); then
|
||||
die "cmake $cmake_ver is too old; Perspective needs >= 3.29.5.
|
||||
A user-level install works: pip3 install --user 'cmake>=3.29.5'"
|
||||
fi
|
||||
|
||||
info "Perspective checkout: $PSP"
|
||||
git -C "$PSP" rev-parse --abbrev-ref HEAD | grep -qx "$BRANCH" \
|
||||
|| echo " ! on branch $(git -C "$PSP" rev-parse --abbrev-ref HEAD), expected $BRANCH"
|
||||
commit=$(git -C "$PSP" rev-parse --short HEAD)
|
||||
dirty=$(git -C "$PSP" status --porcelain | wc -l)
|
||||
echo " commit $commit$([[ $dirty -gt 0 ]] && echo " (+$dirty uncommitted files)")"
|
||||
|
||||
# -- build ------------------------------------------------------------------
|
||||
# `metadata` first: it generates docs/expression_gen.md, which perspective-client
|
||||
# includes at compile time. Building a scope without it fails on the missing file.
|
||||
info "Building (this takes ~40 minutes cold, a few minutes warm)…"
|
||||
( cd "$PSP" && PSP_ONCE=1 PACKAGE="metadata,server,client,viewer,viewer-datagrid" pnpm run build )
|
||||
ok "build complete"
|
||||
|
||||
# -- pack -------------------------------------------------------------------
|
||||
info "Packing tarballs into $VENDOR"
|
||||
rm -f "$VENDOR"/*.tgz
|
||||
for p in "${PACKAGES[@]}"; do
|
||||
( cd "$PSP/$p" && npm pack --pack-destination "$VENDOR" >/dev/null )
|
||||
ok "$(basename "$p")"
|
||||
done
|
||||
|
||||
# -- record provenance ------------------------------------------------------
|
||||
# A committed .tgz is an opaque binary; without this the tie back to source is
|
||||
# only in someone's memory.
|
||||
cat > "$VENDOR/PROVENANCE.txt" <<EOF
|
||||
Built from https://github.com/fleetside72/perspective
|
||||
branch $BRANCH
|
||||
commit $(git -C "$PSP" rev-parse HEAD)
|
||||
based on $(git -C "$PSP" describe --tags --abbrev=0 2>/dev/null || echo 'unknown')
|
||||
built $(date -u +%Y-%m-%dT%H:%M:%SZ) on $(hostname)
|
||||
dirty $dirty uncommitted file(s) in the source tree at build time
|
||||
|
||||
Regenerate with ui/vendor/rebuild-perspective.sh
|
||||
EOF
|
||||
|
||||
echo
|
||||
ls -la "$VENDOR"/*.tgz | awk '{printf " %-52s %5.1f MB\n", $NF, $5/1048576}'
|
||||
echo
|
||||
ok "Done. Now: cd ui && npm install && git add vendor && git commit"
|
||||
[[ $dirty -gt 0 ]] && echo -e "\033[1;33m !\033[0m source tree had uncommitted changes — push them to the fork first"
|
||||
exit 0
|
||||
Loading…
Reference in New Issue
Block a user