Repository navigation
v0.3.0
Release v0.3.0
📦 NPM packages
🐳 Docker image
docker pull ms2data/malloy-publisher:0.3.0
docker pull ms2data/malloy-publisher:latest
ℹ️ Details
- Release branch:
release/sdk-0.3.0 - Triggered by: @sagarswamirao
a dashboard's description is its narrative header, and it renders as markdown
A dashboard could already carry a block of prose and was throwing it away at the last step. Malloy
delivers a doc comment with its newlines and blank lines intact — measured, '## Why this page exists\nRevenue is up but margin is flat.\n\nThe tile below says where it went.' reaches the manifest
exactly like that — and Publisher rendered it as a plain Typography, which collapsed the lot onto
one unstyled line with the asterisks showing.
It renders as markdown now: paragraphs, emphasis, lists, links and inline code. Headings stay at body
weight, because a heading in a description is a section label inside the page rather than a
competitor to the page's own title. The bundled storefront overview has a real one to copy from.
Two things worth knowing when writing one. On a composite the lines are model-level (##"), because
a doc comment attaches to an object and at model level there is none — a #" there fails the package
load with "Object annotation not connected to any object"; the single-query form is the other way
round, with #" attached to its query:. And this is the whole prose surface a dashboard has: per
page, plus a one-line # subtitle per tile. Prose BETWEEN tiles needs a tile kind the format cannot
express yet.
a property on a tile entry is reported instead of dropped
tiles=[intro { kind=text }] compiled, loaded clean, and silently became the tile intro — a run
expression that does not resolve, reported as a query error with no hint that the tag was the
problem. The shape parses today, so an author who has read about tile kinds anywhere can write one
and be told nothing about why it did not work. The package lint now names the property and says where
per-tile presentation actually goes.
a composite dashboard's tiles are cards again
A tile on a ## artifact { tiles=[…] } dashboard painted MUI's white instead of the instance theme's
tile colour, which the theme itself describes as "a faint tint so tiles read as recessed cards on
the page". The single-query # dashboard form has always painted it. On a theme whose page is also
white, that was the difference between tiles that read as cards and tiles that read as nothing, and
it is the visible half of #1069's "they render differently".
Measured on the grid/tiled fixture pair, which is the same layout authored both ways: radius
(4px), padding (20px), border, shadow and grid gap already agreed, and the background was the last
piece that did not. Both forms are now pinned against one sentinel colour in the browser suite, so
neither can drift from the other unnoticed.
Height stays deliberately different: a composite tile is capped so a grid of independent queries
keeps even rows, where the single-query form sizes to its content.
a result panel is sized by what the renderer says it is, not by its DOM
A panel decided its height by walking three levels into @malloydata/render's output and reading
whichever node it landed on, plus a class-name check on .malloy-dashboard for the one shape that
needed a fourth. The renderer publishes no size API, so something has to be measured — but WHICH
rule applies is now read from the renderer's own metadata rather than from its markup, and every
height decision lives in one module with tests instead of four unreconciled constants across as
many files.
Two visible fixes come out of it:
- A KPI row no longer sits in a band of empty space. A
# big_valuetile measured once, lost
the race with the renderer's layout, and recorded the box it had been handed. Measured: a tile
whose content is 136px tall kept the full 400px cap. Roots that have a height of their own —
tables,# dashboardgrids, big values, lists, maps — are now re-measured until they settle. - A chart is no longer measured at all. A chart fills the box it is given and reports back an
inset of it (measured: 392 in a 400px tile, 692 in a 700px cell), so measuring one only ever fed
its own height back to it. The single-query dashboard form used to ask for a 20000px "no cap"
height, which a bare# bar_chartpainted at and then kept — 1992px for a two-row chart. "No
cap" is now the absence of a cap rather than a number standing in for one, and a chart takes the
caller's height.
Which rule a result follows comes from the root's render plugin where there is one:
sizingStrategy: "fill" | "fixed" is the renderer answering this exact question, and a host's own
plugin is placed by its own declaration rather than by a name this SDK has to recognise. A table and
a # dashboard grid have no plugin, so renderAs() still carries those.
The bundled storefront dashboard also gives its map row six and six columns instead of eight and
four. A root # shape_map is drawn at a fixed 588px wide — the renderer builds its spec from a
hardcoded 500x350 and no tag reads those numbers — so a narrower tile clips it rather than shrinking
it, and at four of twelve the east coast and the whole legend were cut off. This is the example
fitting the renderer's constant, not a fix for it: below a grid of about 1220px it clips again.
For SDK consumers: ResultContainer's maxHeight is optional now, and leaving it out means no cap.
RenderedResult gains an onSizing callback reporting which rule a result follows, and its
onSizeChange fires only for results that have a height of their own. Each rendered result also
carries data-malloy-render-as and data-malloy-sizing on its stage, so a panel at an unexpected
height says which rule it took.
a dashboard imports its givens file whole
The bundled examples, the dashboards doc and the malloy-dashboards skill all named the givens a
dashboard uses (import { CATEGORY, BRAND, … } from '../givens.malloy'). They import the file whole
now. A control renders for a given the tiles actually reference, not for every one in scope, so
the whole-file form brings no controls you did not ask for, and a named list only gives an author
something to forget — with a missing control, not an error, as the result. It is also what Malloyyo
documents for the same format, so a repo written for either side reads the same. Sources are
unchanged and still named individually, which is the right form where a file wants a few specific
things.
Nothing about where a given is declared changes: that is the model, and givens.malloy is where a
package keeps it, because the MCP surface, row-level access and #(authorize) all read it. The doc
now also records that declaring one in a dashboard file works — the control renders and the tile
filters — for a page that owns its own knob. That is the exception, not the convention.
dropdowns over a gated source load their options
A control=select whose suggest read a source gated by #(authorize) (or scoped by a
source-level where: on a given) resolved to "Options unavailable": the option query carried no
givens, so the gate denied it. The server now names, per suggest, the givens its source is gated or
scoped by (GivenSuggest.givenNames, also on a named suggest { query=… }'s own references), and
the SDK sends the current values of exactly those with the option query and keys its cache on them.
No other applied filter is sent, so the option list still does not depend on the rest of the page.
useSuggestOptions takes the applied values as a new trailing optional argument; a caller that
omits it, or a server too old to name the givens, behaves as before.
a control with a starting value can be cleared
A given seeded by # artifact { givens { … } } (or a notebook's ## givens { … }) could not be
cleared: the × dropped it from the URL, the host fed that URL back in, and the control snapped
back to the starting value. The hook now recognises its own report arriving back through the host
and keeps the reader's edits across it, while a URL it did not write (a drill landing, the Back
button, a pasted link) still resets the controls as before. The limitation was documented as
unreachable when no server populated starting values; both dashboards and notebooks have since.
ApiErrorDisplay is exported; two internal names are not
@malloy-publisher/sdk now exports ApiErrorDisplay and its props type. Dashboard,
DashboardTile and Notebook all present request failures through it, and the SDK README has
shown it in examples for some time, but a host could not import it to match. Two names that leaked
through the barrels without being documented are no longer exported: SourceExplorerComponent
(the inner half of SourcesExplorer, which is the component to use) and makeDimensionKey (an
internal of useDimensionalFilterRangeData; getDimensionKey stays).
the pages/ URL alias is gone
Data apps were renamed from pages/<file> to data-apps/<file> in 0.0.242, and that release
promised the old spelling would stop redirecting one release later. It kept redirecting for
twenty-five. It stops now: the Console no longer rewrites /<env>/<pkg>/pages/<file> to the new URL,
and the server no longer treats pages as an app route. A bookmark on the old spelling 404s, and
a package that ships its own public/pages/ directory has those files back at
/<env>/<pkg>/pages/<file>, which the alias had been shadowing.
the Workbook editor is gone; storage is now DocumentStorage
Removed: the Workbook editor. Workbook, WorkbookList, WorkbookManager, and
AnalyzePackageButton are no longer exported from @malloy-publisher/sdk, and the Console's
/<env>/<pkg>/workbook/<workspace>/<path> route is gone. The editor saved its own JSON to browser
storage, could not write a .malloynb, and had no link to it anywhere in the Console since August
2025: a notebook click has opened the read-only Notebook view all along. Authoring returns with
the dashboard builder, which will cover the notebook case with text tiles in a one-column layout.
The @uiw/react-md-editor dependency and the @malloy-publisher/sdk/markdown-editor.css export
went with it; styles.css no longer imports that stylesheet.
Renamed: WorkbookStorage is DocumentStorage. The storage seam the editor used stays, as the
interface a host hands the SDK for whatever it authors: DocumentStorage, DocumentStorageProvider,
useDocumentStorage, BrowserDocumentStorage, DocumentLocator, and Workspace, with methods
named listDocuments, getDocument, saveDocument, deleteDocument, moveDocument. A locator now
carries a type ("dashboard" or "notebook"), and listDocuments can filter on it. The browser
implementation namespaces its keys under publisher:document:, so it lists only its own documents
rather than everything the page keeps in localStorage.
Breaking for hosts of @malloy-publisher/app: createMalloyRouter's second argument and
MalloyPublisherApp's workbookStorage prop are now documentStorage, and both are optional,
defaulting to BrowserDocumentStorage. A host that passed a WorkbookStorage implementation renames
its methods and adds type to its locators. No known host did.
dashboards written for Malloyyo look the same here
Three behaviors that differed on identical Malloy between Publisher and
Malloyyo, found by checking Publisher's port against
Malloyyo 0.2.44:
- A composite tile that is one row of measures renders as KPI cards, the way Malloyyo splices the
same tile into its grid, rather than as a one-row table.# big_valueon the view still says so
explicitly; any render tag on the view,# tableincluded, is respected as written. - A
filter<date>orfilter<timestamp>given gets a time-range control: Today, the last 7, 30
or 90 days, the last 12 months, or a custom range of days. The presets are spelled in Malloy's
filter grammar (7 days,12 months), the same values Malloyyo writes, so a URL from either host
reads on the other. A custom range is inclusive on both ends as picked; a single day keeps the date
picker, and any other filter keeps the text box, as written. - The
range_min/range_maxslider has two handles and writes an inclusive range,[10 to 20].
With the upper handle at the ceiling it writes the lower bound alone,>= 10, which is what the
one-handled slider wrote, so existing links and starting values still place the handles.
And one thing that was silent now speaks: an # artifact tag on a source view, which Malloyyo
serves and Publisher never read, made the file a shared include with nothing reported. It is now a
package warning that names the two forms Publisher does read. The dated comparison lives in
docs/malloyyo-dashboards-design.md.
What's Changed
- chore(release): stamp 0.2.7 on main by @housejester in #1142
- fix(server,sdk): stop returning connection credentials from the API by @mlennie in #1071
- fix(dashboards): close the behavioral drift from Malloyyo 0.2.44 by @kylenesbit in #1145
- feat!(sdk,app): remove the Workbook editor; WorkbookStorage becomes DocumentStorage by @kylenesbit in #1146
- chore(examples): bring data-app onto the current SDK and workspace versions by @kylenesbit in #1149
- chore(app,server): retire the
pages/alias for data apps by @kylenesbit in #1147 - chore(sdk): export ApiErrorDisplay; stop exporting two internals by @kylenesbit in #1148
- fix(sdk): let a given with a starting value be cleared by @kylenesbit in #1150
- refactor(sdk,app): share drill-to-self and URL-param code between Dashboard and Notebook by @kylenesbit in #1151
- fix(sdk): keep a cleared control cleared while the URL catches up by @kylenesbit in #1154
- fix(server,sdk): let a suggest over a gated source load its options by @kylenesbit in #1152
- fix(sdk): size a result panel by what the renderer says it is, not by its DOM by @kylenesbit in #1153
- docs(dashboards): import a package's givens file whole, not by name by @kylenesbit in #1155
- skills(malloy-analysis): rules an eval traced to real failures by @jswir in #1137
- fix(sdk): paint a composite tile with the theme's tile colour by @kylenesbit in #1156
- feat(dashboards): a narrative header, and a tile property that no longer vanishes by @kylenesbit in #1157
- fix(boundary): admit a derivation that composes through a named query by @sagarswamirao in #1159
- fix(skills): make an eval run say what it measured, and fail loudly when it cannot by @jswir in #1114
Full Changelog: v0.2.7...v0.3.0