Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/app/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { LineagePage } from "./pages/LineagePage";
import { LineageEntryPage } from "./pages/LineageEntryPage";
import { DiffPage } from "./pages/DiffPage";
import { CachePage } from "./pages/CachePage";
import { CustomizationsPage } from "./pages/CustomizationsPage";
import { EmptyStatePage } from "./pages/EmptyStatePage";
import { ProjectsPage } from "./pages/ProjectsPage";
import { api } from "./api";
Expand Down Expand Up @@ -55,6 +56,7 @@ export default function App() {
<Route path="lineage" element={<LineagePage />} />
<Route path="lineage/:hash" element={<LineageEntryPage />} />
<Route path="cache" element={<CachePage />} />
<Route path="customizations" element={<CustomizationsPage />} />
<Route path="diff/:alias" element={<DiffPage />} />
<Route path="diff/:alias/:va/:vb" element={<DiffPage />} />
<Route path="*" element={<Navigate to="catalog" replace />} />
Expand Down
4 changes: 4 additions & 0 deletions packages/app/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type {
LineageLayout,
DiffData,
ResultCache,
Customizations,
Projects,
} from "./types";

Expand Down Expand Up @@ -132,6 +133,9 @@ export const api = {
resultCache: (project: string): Promise<ResultCache> =>
get(`/${project}/api/result_cache`),

customizations: (project: string): Promise<Customizations> =>
get(`/${project}/api/customizations`),

deleteResultCache: (project: string, hash: string): Promise<{ ok: boolean; hash: string }> =>
fetch(`/${project}/api/result_cache/${hash}`, { method: "DELETE" }).then((r) => {
if (!r.ok) throw new Error(`delete failed: HTTP ${r.status}`);
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/components/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export function Header() {
<Link to={`/${project}/notebook`}>notebook</Link>
<Link to={`/${project}/lineage`}>lineage</Link>
<Link to={`/${project}/cache`}>cache</Link>
<Link to={`/${project}/customizations`}>customizations</Link>
<Link to="/projects">projects</Link>
</nav>
)}
Expand Down
99 changes: 99 additions & 0 deletions packages/app/src/pages/CustomizationsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { api } from "../api";
import { useSSE } from "../SSEContext";
import type { CustomizationItem, Customizations } from "../types";

const SECTIONS: { key: keyof Omit<Customizations, "project">; title: string; blurb: string }[] = [
{
key: "summary_stats",
title: "Summary stats",
blurb: "Per-column statistics computed by Buckaroo. def compute(col) -> ibis.Expr",
},
{
key: "display_klasses",
title: "Styling classes",
blurb: "ColAnalysis subclasses that control table rendering and pinned rows.",
},
{
key: "post_processings",
title: "Post-processing",
blurb: "Expression transformers offered in Buckaroo's post-processing dropdown. def process(expr) -> ibis.Expr | DataFrame",
},
];

function Section({ title, blurb, items }: { title: string; blurb: string; items: CustomizationItem[] }) {
const active = items.filter((i) => !i.disabled).length;
return (
<div className="cust-section">
<div className="cust-section-head">
<strong>{title}</strong>
<span className="meta">
{items.length === 0
? "none"
: `${active} active${items.length - active ? ` · ${items.length - active} disabled` : ""}`}
</span>
</div>
<span className="meta cache-note">{blurb}</span>

{items.length === 0 ? (
<div className="nb-empty">none defined</div>
) : (
items.map((item) => (
<details key={item.path} className="cust-item">
<summary>
<span className={`cust-name${item.disabled ? " muted" : ""}`}>{item.name}</span>
{item.disabled && <span className="cust-flag">disabled</span>}
<span className="meta cust-path" title={item.path}>
{item.path}
</span>
</summary>
<pre className="code">{item.source}</pre>
</details>
))
)}
</div>
);
}

export function CustomizationsPage() {
const { project } = useParams<{ project: string }>();
const { version } = useSSE();
const [data, setData] = useState<Customizations | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");

useEffect(() => {
if (!project) return;
// Refetch on `version` bumps but keep the current data on screen — don't
// flip back to the loading state, so an SSE event doesn't flash the page.
api
.customizations(project)
.then((d) => { setData(d); setError(""); setLoading(false); })
.catch((e) => { setError(String(e)); setLoading(false); });
}, [project, version]);

return (
<main style={{ overflow: "auto" }}>
<div className="cache-pane">
<div className="cache-summary">
<strong>Catalog customizations</strong>
<span className="meta cache-note">
Project-wide Buckaroo customizations. They apply to every entry in the catalog and are loaded
into each session. Read-only here — edit via the MCP tools.
</span>
</div>

{loading ? (
<div className="meta" style={{ padding: 16 }}>loading…</div>
) : error && !data ? (
<div className="error-banner">{error}</div>
) : (
SECTIONS.map((s) => (
<Section key={s.key} title={s.title} blurb={s.blurb} items={data ? data[s.key] : []} />
))
)}
</div>
</main>
);
}
16 changes: 16 additions & 0 deletions packages/app/src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -514,6 +514,22 @@ table.cache-table .muted { color: var(--muted); }
.del-btn:hover { background: #2a1b1b; color: #ffb4b4; border-color: #7a4a4a; }
.del-btn:disabled { opacity: 0.5; cursor: default; }

/* catalog customizations page */
.cust-section { margin-bottom: 22px; }
.cust-section-head { display: flex; align-items: baseline; gap: 12px; margin-bottom: 2px; }
.cust-section-head strong { font-size: 14px; }
.cust-section .cache-note { display: block; margin-bottom: 8px; }
.cust-item { border: 1px solid var(--border); border-radius: 5px; margin-bottom: 6px; background: var(--panel, transparent); }
.cust-item > summary { display: flex; align-items: baseline; gap: 10px; padding: 6px 10px; cursor: pointer; list-style: none; }
.cust-item > summary::-webkit-details-marker { display: none; }
.cust-item > summary::before { content: "▸"; color: var(--muted); font-size: 11px; }
.cust-item[open] > summary::before { content: "▾"; }
.cust-item .cust-name { font-weight: 600; font-family: ui-monospace, monospace; }
.cust-item .cust-name.muted { color: var(--muted); font-weight: 400; text-decoration: line-through; }
.cust-item .cust-path { margin-left: auto; font-family: ui-monospace, monospace; font-size: 11px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 50%; }
.cust-item .cust-flag { color: var(--muted); border: 1px solid var(--border); padding: 0 6px; border-radius: 10px; font-size: 11px; }
.cust-item > pre.code { margin: 0; border-top: 1px solid var(--border); border-radius: 0 0 5px 5px; }

/* projects management page */
.projects-page { max-width: 600px; margin: 32px auto; padding: 16px; }
.projects-page h2 { margin-top: 0; margin-bottom: 16px; }
Expand Down
14 changes: 14 additions & 0 deletions packages/app/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,20 @@ export interface ResultCache {
total_formatted: string;
}

export interface CustomizationItem {
name: string;
path: string;
source: string;
disabled: boolean;
}

export interface Customizations {
project: string;
summary_stats: CustomizationItem[];
display_klasses: CustomizationItem[];
post_processings: CustomizationItem[];
}

export interface Projects {
active: string | null;
available: string[];
Expand Down
17 changes: 17 additions & 0 deletions src/tallyman_companion/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1007,6 +1007,23 @@ def api_result_cache(project: str):
"total_formatted": _fmt_bytes(total),
}

@app.get("/{project}/api/customizations")
def api_customizations(project: str):
"""Project-wide buckaroo customizations: summary stats, display
klasses and post-processing functions, each with its source. These
are global to the catalog (one set per project), not per-entry."""
project = _validate_project(project)
from tallyman_core.display_klasses import list_display_klasses # noqa: PLC0415
from tallyman_core.post_processing import list_post_processings # noqa: PLC0415
from tallyman_core.summary_stats import list_stats # noqa: PLC0415

return {
"project": project,
"summary_stats": list_stats(project),
"display_klasses": list_display_klasses(project),
"post_processings": list_post_processings(project),
}

@app.delete("/{project}/api/result_cache/{content_hash}")
async def api_delete_result_cache(project: str, content_hash: str):
project = _validate_project(project)
Expand Down
Loading