Repository navigation
v0.9.4
Release v0.9.4
📦 NPM packages
🐳 Docker image
docker pull ms2data/malloy-publisher:0.9.4
docker pull ms2data/malloy-publisher:latest
ℹ️ Details
- Release branch:
release/sdk-0.9.4 - Triggered by: @kylenesbit
Compile returns the document it describes
POST …/models/{path}/compile at scope append now answers a source that carries a model-level ## artifact tag with a document: the kind, the manifest and the cells the same text would serve once saved, read from the submitted text alone. A tile or cell the caller may not read (#(authorize)) is not compiled and comes back restricted: true with no diagnostic. Given gains secure, true for a #(secure) declaration.
The SDK builder opens a document held as text: <DashboardEditor textSource={{ modelPath, hiddenGivens }} /> and <DashboardView preamble runModelPath hiddenGivens /> run every tile, cell and control option as the viewer's own text. Existing responses and props are unchanged. malloy-notebook-chat is no longer part of the shared skills; it only makes sense in a host that binds a chat to a notebook.
Submitted text has no folder to take a kind from, so a ## artifact tag that names no kind is a dashboard when it lists tiles=[…] or carries a dashboard { … } grid tag, and a notebook otherwise; the builder's "Show as: Dashboard" now writes kind=dashboard for a document held as text.
The query route's 404 for a source a gated model hides now reads Query target is not queryable. in place of No queryable source "x"., the sentence it already gave for a source that is absent, so the wording cannot tell the two apart.
The notebook GET now serves a layout notebook's (tiles=[…]) query cells like any other: each carries queryInfo and proseLines: [], where it carried neither, so a client no longer reads its cells as withheld. A cell the surface refuses still carries no queryInfo.
Compile at scope append now refuses document text (a source carrying a model-level ## artifact tag) that writes # image or # link (the render tags that make a cell value a URL the viewer's browser loads) or HTML inside a # label, that reads @env. in an annotation or a ## note, that writes an annotation the tag parser rejects or that is over 8,192 characters, or that frees a column with except: or rename: and then declares one of the same name in the same extend chain, or that carries more than 1,000 annotations or 64 KB of annotation text; each is a 400 with the same message whether or not the data exists. Plain append text without that tag, and a model file, keep the tags they define; existing callers of compile_model see no change. This removes the direct author-written paths; it does not close the channel. The renderer has other sinks that draw a data value as HTML or a URL (a chart x-axis label is measured through innerHTML, table headers too), and the except-and-redeclare refusal is partial: it does not cover a source whose tagged expression reads a parameter or a given, or a route the token scan does not follow. A # label is refused only when it holds markup that a > closes. A host that opens documents held as text should run the viewer under a Content-Security-Policy. In text-source mode the SDK also removes # image, # link and markup-bearing # label properties from the result annotations it draws, model-defined fields included; other properties on the line are kept, and a line that reads @env. or does not parse is dropped whole. A consumer that renders query results with @malloydata/render itself gets none of that. The SDK's markdown (Prose, so text tiles and descriptions) no longer parses raw HTML: tags such as <iframe>, <form> and <meta> render as text.
Packages that version on their own line
@malloy-publisher/skills and @malloy-publisher/create-malloy-package are not part of the lockstep version above, and their notes do not belong in this file. The release workflow still publishes them: for each one it reads the version from main and, when that version is not yet on npm, dispatches that package's own publish workflow (skills-npm.yml, create-malloy-package-npm.yml). A package whose version is unchanged is skipped, so a release that touched neither is unaffected.
To ship one of them, bump its package.json on main and run an ordinary release. The bump is what triggers the publish and nothing else in CI requires it, so a change that lands without one is skipped and the release stays green. A prerelease skips both packages outright, since their own versions carry no hyphen and would take over the latest tag. Either way the release's job summary says what it skipped and why. Each package can also still be published on its own by dispatching its workflow directly, which is the point of keeping them separate: a skill edit should not need a server release.
One behaviour change to know about: skills-npm.yml now publishes only from main, matching the guard create-malloy-package-npm.yml already had. Dispatching it from a branch still runs check_pack, but the publish job is skipped, and a skipped job reports success, so check the job list rather than the run's green tick if you expected a publish. See .github/workflows/CONTEXT.md for the publishing rules that are easy to get wrong.
SDK: the renderer loads only when a result does, and dashboards run only the tiles near the screen
- No renderer download at idle.
RenderedResultno longer starts importing@malloydata/render
(about 3.4 MB, 1 MB gzipped) the moment its module is evaluated, which made every page of a host
that loaded the main entry download it whether or not it drew a result. A result panel now starts
the import when its query is in flight, so the first chart still does not wait on it. A host that
stripped the old statement at build time can drop that patch; one that fails its build when the
statement is missing will now fail and should drop the check. - Offscreen dashboard tiles wait. A dashboard or notebook-layout tile runs its query once it is
within about 600px of the viewport, rather than on mount, so a long dashboard no longer bills the
warehouse for tiles nobody scrolls to. The distance is measured in whatever box scrolls the page
(the Console scrolls inside one), and printing runs every tile. WhereIntersectionObserveris
unavailable every tile runs as before. A tile waiting to come near reads "Loading…", not
"Running…". - Superseded queries are cancelled. Tile, result, suggestion and filter-value queries pass
react-query's abort signal to the request, so a query whose givens changed or whose tile unmounted
is aborted instead of finishing. A cancelled query shows as still loading, never as an error. The
server cancels the query with it: when a client disconnects fromPOST …/models/{path}/query,
the query's abort signal fires (the path a query timeout already takes), so the database stops
work that nobody will read rather than running on after its concurrency slot was released. usePublisherThemefrom@malloy-publisher/sdk/client. The light entry now exports it (and the
Theme,ThemeModeandResolvedThemetypes), so a host following the SDK's colour mode at its
root no longer needs the main entry on its critical path.- No refetch on refocus. The SDK's
globalQueryClientno longer refetches every stale query
when the tab regains focus, including the package, model and status queries of a host that shares
that client. Retries stay off. What to do: a host that relied on a refresh on refocus calls
invalidateQueriesitself.
cloning a GitHub package no longer passes GIT_* variables to git
simple-git moves from 3.36 to 4.0 to clear three advisories, and 4.0 filters the environment it
passes to git. When Publisher clones a package or environment from a GitHub URL, the git child
process no longer sees ambient GIT_* variables (GIT_SSL_CAINFO, GIT_TERMINAL_PROMPT,
GIT_CONFIG_* and the rest), or SSH_ASKPASS, EDITOR, VISUAL and PAGER. Proxy variables
(HTTPS_PROXY, NO_PROXY), SSL_CERT_FILE and HOME still pass through, so settings in the
server user's ~/.gitconfig still apply. An operator who pointed clones at a private CA with
GIT_SSL_CAINFO should set http.sslCAInfo in that gitconfig instead.
(BREAKING) — A calmer dashboard and notebook builder, and one theme accent
The builder is the read-only page in a second state, with fewer controls around it.
- Edit and View are in the Console's header, beside the breadcrumbs, on every dashboard and notebook page. View returns to the read-only page and asks first if there are unsaved edits. The page's own Edit bar, and the "Back to …" links on every page, are gone. Reading and editing have the same header and margins, and dashboards, notebooks and data apps share one page width.
- Save saves in place. The builder stays open after a save, and the notice after a save (View change, Undo save) is removed; undo and redo still step through edits. The first save of a cell-format notebook, which rewrites it in the tile layout, asks first.
- The builder's actions sit on the title's line: + Tile, undo, redo and Save. + Filter is a chip after the filter chips.
- Settings is gone. A document stays the kind it was created as. A tile's width is set by dragging its right edge, or by focusing that edge and pressing the arrow keys (Home and End for one column and the full grid); the tile menu's width presets, Grid width and "Run as controls change" are removed, and a file's own
columnsandautorunare kept. Adding a tile imports its source by name when the file cannot see it yet, so there is no Sources list. - Tile menu: Drill, Delete, and a viz type that applies as it is picked. Editable titles, subtitles and descriptions carry a small pencil after their text. The description is drawn as the document's text tiles are, in both modes (a dashboard boxes it, a notebook leaves it bare), and selected like a tile.
- Dashboard tiles no longer offer Explore.
Theme. The Console's accent — primary buttons, sliders, the builder's selection — is the palette's first series colour (ResolvedTheme.accent, accentHover, accentContrast, and accentFor), kept as picked when it reads against the page at 3:1 and otherwise moved only as far as it takes. In dark mode a series colour too dark for the canvas is lifted the same way, and a map colour set only for light is carried into dark. Drill links keep the Console's default blue. The light/dark toggle and the theme editor link moved into the sidebar. Scatter charts do not follow the palette yet: @malloydata/render writes a fixed colour into their spec.
Themeable chart and table chrome (not breaking). Six new per-mode palette keys set colours that were fixed until now: border (table gridlines), cardBorder (a dashboard card's edge and a pinned table header's rule), axis (chart axis domain and ticks), gridline (chart gridlines), chartText (chart axis, legend and title text) and value (a KPI tile's number). Their defaults are the colours drawn before, so an unthemed page looks the same. Set them in publisher.config.json's theme.palette, the Theme editor (Charts and Tables), or a # theme.palette.<key>.{light,dark} annotation; a host that overrode these with CSS can set them through the theme instead. ResolvedTheme gains gridline; foreground, axisFaint and valueColor keep their names and now read chartText, axis and value.
For SDK embedders (BREAKING). The viewer and the builder are separate in both directions: the builder reuses the viewer's pieces, the viewer imports nothing from the builder, and switching between them is the host's.
onExitis optional. Without it,DashboardEditor,NotebookEditorandDashboardBuilderdraw no way out, and leaving is the host's (the Console's is View in its header): draw your own and guard it withonDirtyChange;UnsavedChangesDialogis still exported. With it, the toolbar shows Close after Save, and Close on unsaved work asks first, as before. What to do: nothing, if you pass it.onSaveNoticeChangeis removed fromDashboardBuilder, with the save notice. What to do: drop the prop; there is no notice to hold a newer version behind.DashboardBaris removed. What to do: draw your own bar; nothing in the SDK used it.- The builder's Show as switch is removed. What to do: nothing; a document's kind is chosen when it is created (
NewDocumentDialog'sallowKindChangeis unchanged). Dashboardno longer offers Explore from here on its tiles. What to do: if you want it, renderDashboardTilewith itsonExploreprop, which is unchanged.Notebookno longer caps its width at 1200px or pads its sides; it fills its container, asDashboarddoes. What to do: give it a container with the width you want.DataAppViewerlays out at 1600px.- New:
Package'sonOpenDocument({ kind, slug, mode })hands the host the dashboard or notebook to open, toviewor toedit. Without it,Packagenavigates to the Console's routes as before.
Consistency and polish.
- Destructive text (Delete on a tile, Remove filter) takes the host MUI theme's error palette —
error.darkin light mode,error.mainin dark — through the exporteddangerTextColor, so a host with its own error red gets it. - Filter wording is one set: the chip's ×, and the filter window's Remove filter; the drill window is titled Drill, matching the tile menu.
- New exported tokens:
MOTION_FAST,reducedMotionSx,scrollBehavior,visibleWithoutHoverSx,ResolvedTheme.shadow(lift,drag),LOADING_COPY,STICKY_CONTROLS_Z,TILE_MIN_HEIGHT, andMONO_FONT_FAMILY(which the Console now reads from the SDK). Motion respectsprefers-reduced-motion. TileCardtakeskind(queryortext), which sets its floor; the builder's text tiles and description use it.Notebook's embed dialog is removed; its copy-link button remains. The model page's copy link is a labelled button.- No bold: headings and labels use the theme's medium weight, as Credible's do.
- Prose (package READMEs, notebook markdown, descriptions, text tiles) is set in the instance theme's
font.family, as tile titles are, rather than the host MUI theme's font. The filter panel's Apply and Reset take the host theme's button casing. - Notebook blocks have one clear space around them when hovered or selected: the ring stands 12px off bare content, its menu sits on the ring's edge rather than over the text, notebook tiles are 40px apart in both modes, and document prose is flush at its top and bottom so a text tile lines up with a chart tile's title.
- The builder's undo and redo shortcuts do nothing while a dialog or menu is open.
- The builder's save state is announced to screen readers, and a tile's resize edge is a focusable separator with its width as its value.
DashboardBuilderandDashboardEditorare split into hooks and parts; their exports and props are unchanged.
Examples. storefront gains notebooks/overview.malloy, the overview dashboard as a notebook with text between the charts, and no longer pins its own chart palette, so it follows the instance theme.
The Docker image is signed with cosign
ms2data/malloy-publisher is now signed at release with Sigstore cosign (keyless, through GitHub Actions OIDC). Verify a release with:
cosign verify ms2data/malloy-publisher:<version> \
--certificate-identity https://github.com/malloydata/publisher/.github/workflows/docker-image.yml@refs/heads/main \
--certificate-oidc-issuer https://token.actions.githubusercontent.comUse cosign v3 or later: the signature is a Sigstore bundle stored as an OCI referrer, which an older cosign or a policy engine that only looks for a .sig tag does not find.
The signature is on the multi-platform manifest list, so it also covers the per-platform images and their SBOM and provenance attestations. Earlier releases are unsigned. See packages/server/README.docker.md.
Malloy 0.0.435: Postgres sessions close when a query fails, and Trino map and json columns return their values
Publisher now builds on @malloydata/* 0.0.435, up from 0.0.434. No Malloy API that Publisher calls changed, and a model that compiled on 0.0.434 compiles the same way, except a Trino or Presto source on a table whose DESCRIBE returns no columns (the last item below). The changes that reach a running server are in the Postgres and Trino drivers:
- A Postgres query that fails no longer leaves its session open (malloydata/malloy#3109). On 0.0.434 the driver closed its database session only after a query succeeded. A query that failed after connecting (malformed SQL, a permission error, a column type the driver cannot map) left the session open on the database until the server's idle timeout or an operator ended it. Every plain Postgres connection was affected, and so was the schema lookup and row streaming on an SSH-proxied one, so a model or agent that kept retrying a failing query could use up the database's
max_connections. Sessions now close on failure, and when a consumer stops reading a stream early. If you have been seeing idle Publisher sessions pile up inpg_stat_activity, this is the likely cause. - A Postgres session the database drops mid-query no longer raises an uncaught exception (malloydata/malloy#3109). When the connection was cut (a failover,
pg_terminate_backend, a network drop), the driver emitted anerrorevent that nothing handled. Publisher does not catch uncaught exceptions, so that event could stop the server process. The query now fails with the connection error and the server keeps running. - Trino
mapandjsoncolumns return their values (malloydata/malloy#3100). On 0.0.434 a column of either type read back asnullin every row. It now returns the object or parsed JSON document the Trino client decoded. A data app or dashboard that showed nothing for such a column will start showing values, and code that relied on thenullwill see an object. - A Trino or Presto table whose
DESCRIBEreturns no columns is now an error (malloydata/malloy#3091). On 0.0.434 the empty result was cached as a table with no fields, so the source compiled and every field reference failed with'<field>' is not defined, and stayed that way until the cache was cleared. The source now fails to compile withCould not fetch schema for table <name>: DESCRIBE returned no columns, nothing is cached, and the next compile tries again.
The release also adds an experimental SQL Server dialect. Publisher does not offer a SQL Server connection type, so it has no effect here.
Semantic retrieval: no lexical fallback when embeddings are configured, new embeddingIndex.status values, and optional LLM keyphrases, summaries, refine, rerank and source matching
With no embedding provider and no LLM configured, get_context ranks by words as before, and a listing request is unchanged. Everything below applies once a provider is configured. Read the first four items if you poll the status API, run a server with an embedding provider, or write publisher.json files.
embeddingIndex.status has new values. On GET /api/v0/environments/{env}/packages/{pkg} the status was indexing | ready | cooldown | too-many-entities, and the field was absent when no provider was configured. It is now always present and is one of lexical | indexing | ready | error:
lexical: no embedding provider is configured. This is a mode of the server, not a failure.error: the index cannot serve. A newreasonfield carries the two cases that used to be statuses of their own,cooldownandtoo-many-entities. Other errors, such as an invalid embedding configuration, have noreason;lastError.messageexplains them. A failure the vendor will not change its answer to (a 4xx such as a 400 for a prompt past the context window; not 401, 403, 408 or 429) has noretryAtand is not tried again until the package's content or retrieval settings change, or the server restarts.- A new
stagefield (keyphraseorsource_summary) says which index-time LLM step failed, when one did.
cooldown and too-many-entities stay in the status enum in api-doc.yaml, marked deprecated, so a client generated from an earlier spec still understands every value. The server never sends them as a status any more. A client that tests status == "cooldown" or "too-many-entities" must now test status == "error" and read reason. A poller that waits for ready or cooldown and nothing else would wait forever on error; stop on ready or error.
With an embedding provider configured, get_context no longer falls back to a lexical ranking. While the index builds it answers retrieval: "indexing" with retrieval_progress {embedded, total} and no results. After a failure it answers retrieval: "error" with a reason that says how to fix it. A caller that treated every ranked response as usable must handle these two values. Listing mode (no search text) works in every state. The index now starts building when a package loads, one package at a time, instead of on the first question, and each batch is stored in its own transaction, so a failed sync keeps the batches before it.
A bad retrieval key in publisher.json now stops that package from loading. Only these keys are accepted: representation, keyphrases, refine, rerank, sourceMatch, sourceSummary and prompts. Any other key fails the package load (HTTP 424) with a message naming the valid keys, instead of being ignored. enabled: true on a stage, on a server with no LLM, fails the package load too. A bad retrieval value in publisher.config.json is reported through /status as initError, like any other unreadable config.
Ranked responses are capped at 35,000 characters. Whole source cards are dropped, never cut in half, and a warning says how many. Narrow the request with model_path, source_name or entity_name to see them.
Also changed, when the matching provider is configured:
- Semantic search covers a source's own fields. Joined copies are made when the answer is assembled and scored
cosine * 0.9 ** (hops + 1). A joined field that cannot be rebuilt from a source of its own stays in the index, so it is still found. A dottedentity_namethat names a joined field, such asbuyer.name, still matches on this path; a barenamedoes not match the joined copy. retrieval.representationdefaults tosingle: one vector per entity, made from its keyphrase, doc or name. Scores and rankings change and each package re-embeds on its first start.facetsis the earlier layout.- With an LLM, refine, rerank and source matching run by default (
enabled: "auto"; setfalseper package to turn one off). A failure in any of them returns an error naming the stage (retrieval_reason: "llm-stage-failed",retrieval_stage) that shows the vendor's status and message and never its endpoint; nothing silently falls back. Refine and rerank can empty the result list on purpose, so an empty answer no longer always means that nothing matched. Each request is limited toretrieval.llm.maxCallsPerRequestchat requests (default 20), retries and JSON repairs included. Source matching sends at most the number of sources the call budget allows per source target, ranked by words shared with the target, and a warning says how many it did not send. A source row nobody rated (source matching off) scores asMEDIUMplus its cosine, and rerank's tiebreak stays under one so a source it rated 1 cannot outrank one it rated 2. - Source summaries. The index sync writes a summary and a one-line summary per source and model file (
retrieval.sourceSummary,autoby default). Two files that each define a source of the same name get one summary each, and a join expands the source in the file it names. Cards then carrysource_info.one_line_summary, and a request shows a stored summary only while it matches the source as it is now. A source with no documentation gets the line "Thenamesource." without a model call. The prompt cuts one field's doc to 500 characters and the whole message to 60,000. - Refined entities carry
matched_targets[].match_reason, the model's one-sentence reason. - A repeated chat failure pauses LLM steps for every package. Three failures in a row that are rate limits, timeouts or server errors (429, 408, 5xx) stop chat calls for 60 seconds, process-wide. Other errors, such as a rejected key or a bad request, do not count towards it.
- New settings.
publisher.config.jsongains aretrievalblock (llm,embedding,egress.preset,indexing.maxEntities), and the API keys areLLM_API_KEYandEMBEDDING_API_KEY.retrieval.indexing.maxEntitiesreplaces the fixed 5,000-entity cap; its default is 5,000. Seedocs/get-context-pipeline.mdanddocs/configuration.md. - Trace.
X-Publisher-Retrieval-Trace: summaryaddsretrieval_traceto a response;PUBLISHER_MCP_TRACE=retrievalon the server writes it to the log instead. It holds per-stage counts and timings.
A capped keyphrase or summary sync resumes on the next restart. When retrieval.llm.maxCallsPerSync stops a sync early, the status can read ready with done below total. A package reload does not continue it, because a reload of an unchanged package starts no sync work.
Console can edit notebooks, and create notebooks and dashboards
A notebook is now a one-column dashboard, edited in the same builder as a dashboard. A tagged
notebooks/*.malloy notebook has an Edit button in the Console, and a dashboard and a notebook
share one set of tools: drag tiles into order, pick a chart, add filters, and add text tiles,
which hold markdown. Dashboards gain text tiles too. Everything is click-to-edit, with no edit icon:
click a title, a description, a text tile or a tile's heading and type. Insert a tile between two
with its +, and switch a document between Dashboard and Notebook under Settings → Show
as: a tag edit on the same file, in place. Switching to Notebook writes kind=notebook and drops
the dashboard { columns=N } line, since a notebook is one column; switching a file under
notebooks/ to Dashboard writes kind=dashboard explicitly, so the server serves it as one.
Imports are edited there as a source picker.
The package page lists dashboards and notebooks in one Artifacts section. Each row shows its title
(or its file name when it has none) and a kind badge, and the New button sits on the section's
heading row. A pair of files sharing a name in different folders shows the folder to tell them apart.
A tile's width is set from its menu, not by dragging its edge. The right-edge drag handle is gone; a drop never changes a tile's width. Pick Full, ½, ⅓ or ¼ from the tile's menu, nudge the selected tile with the arrow keys, or write # colspan=N. The arrow keys no longer change the width while a tile is being moved, and a nudge stops at 24 columns, so nudging a # colspan above 24 sets it to 24.
Grid width has one entry per width, and a ceiling of 24. Settings → Grid width no longer has a "Default (2)" item that duplicated "2" and removed the columns tag; it shows the file's width (2 when unset) and writes columns=N when you pick one. The list is 2, 3, 4, 6 and 12 (the divisors of 12) and 24; a file's own width is still shown. A # dashboard { columns=N } above 24 now draws a package warning, and the reader still renders it.
Save writes at once. There is no review step before a save any more, for notebooks or for
dashboards (a dashboard's Save used to ask first when it added or removed a tile). The save then
shows View change, the file's diff read-only, and Undo save, which writes the file back as
it was. The save target is named under the button, and the exit button reads Close. Wording
elsewhere is plainer: Viz type, Drill-through, "On or after a date", and a Filters
heading over the filter strip.
Cell-format notebooks convert when saved. A notebook written the older way, with (markdown)
cells, opens converted to the tile layout and unsaved; Save writes the layout and Undo save puts the
original text back. Nothing is written until you save. Save writes to the host's record when the host
has one, and otherwise to the package, so the Console on a server that does not take writes has no
Save. A notebook the editor cannot convert opens read-only and says why: a run: that is not
<source> -> <view or body> (an inline extend before the arrow, a source that is not a name,
nothing after the arrow), a refinement of a multi-stage query, a run defined through more than ten
other named queries, a tag value Malloy cannot read (a malformed date such as @2024-13-01), and
the read-level refusals that were already there (a statement above the tag, text after a block
closer, a comment straddling two cells, a lone carriage return). The write route
(PUT …/models/{path}) accepts tagged notebooks/*.malloy as well as dashboards/*.malloy; an
untagged write to notebooks/ is refused with 400. Two refusals are new: a dashboard whose name
another file already holds answers 409 and nothing is written, and a file whose compiled model carries
no ## artifact tag (a tag only inside a block comment) is rolled back and answers 500 where it
used to land unserved; a dashboards/ file with no artifact property on any # or ## tag line
is refused with 400 before it compiles. A tagged dashboard with no tiles still saves.
What a file is comes from its tag, not its folder. A document's kind is its ## artifact tag
(dashboard or notebook), and a tagged file is listed and served from wherever the package puts
it. A file whose tag names no kind is edited as its folder's kind (notebooks/ is a notebook).
The tag can be written in block form (##|), closed by |## on a line of its own: as Malloy reads
it, a |## on the opener's line closes nothing, and the server's check of an unsaved file now
agrees. Notebooks are authored as layouts
(tiles=[…]), the shape the builder writes, and a layout notebook's read view has no cards around
its tiles.
A tile that ignores a filter says so. In the reader and the builder, a tile whose query never reads one of the page's controls shows an amber "Doesn't respond to Brand" chip under its heading, and a tile that reads them all shows nothing; DashboardTile.givenNames is now what the tile's compiled query reads (a joined source's where:, a $X dimension and the source's gates included, and refinements resolved), where it used to be a static walk of the view, on the dashboard and notebook routes alike. A given only a gate reads is now sent with the tile when the host supplies it, rather than dropped, but does not become a control.
API contract. DashboardTile gains kind (query or text), name and markdown; query is
absent on a text tile, so a client that runs a dashboard's tiles should skip kind=text tiles when
running queries, and a text tile's colspan and break come from its tiles=[…] entry.
DashboardManifest gains kind (always dashboard on GET …/dashboards/{name}), and
RawNotebook gains dashboard, the layout of a notebook written as tiles, with kind notebook
and one column.
For hosts that mount the builders.
DashboardBuilder'sonSaveis nowonSave(source, { purpose, document }), wherepurposeis"save"or"undo"anddocumentis the document the written text holds. Undo save calls it too, with the text from before the save, so write through the same channel and checks.SaveContextandSaveHandlerare exported from@malloy-publisher/sdk/builder.- The notice prop is
onSaveNoticeChange(showing): hold back replacing the open document while the View change / Undo save notice is up. - Undo save reports
notebook.save_undoneordashboard.save_undone, andnotebook.save_undo_refusedordashboard.save_undo_refusedwhen the write is refused;BuilderEventis the union of the dashboard and notebook events. - A
DocumentStoragehost sees Undo save as an ordinarysaveDocumentof the earlier text. The host store has no compare-and-swap, so a newer version written elsewhere is overwritten. DashboardEditortakespathandkind, to open a document as its tag's kind from the path the package lists it at.NotebookEditoris now a wrapper over the dashboard builder withkind="notebook", and the cell-notebook builder is gone.useServer().mutableisboolean | undefined:undefinedwhile/statusloads or when it failed, and the editor offers no package Save until it istrue.NewDocumentDialogis exported from the main entry, with new propsallowKindChange,savedAs,modelsLoading,modelsErrorandonRetryModels. It names a model it could not read and offers Retry only whencanRetryRequest(also exported;useDocumentChoicesreturns its answer as thecanRetryboolean) says a second attempt can succeed, which is never after a 401, 403 or 404. A disabled Create says why in its tooltip and accessible name; a blank title no longer disables it and is reported on press. It reads "Creating dashboard…" while it writes, and Escape or a backdrop click does not close it then. A picked view that leaves the list stays unpicked rather than being swapped for another.@malloy-publisher/sdk/textis a new entry with no dependencies, exportingartifactTag, itsArtifactTagtype,splitSourceLinesandcanRetryRequest, for a host that reads a file's tag without loading MUI or the Malloy parser.artifactTagfindsartifactanywhere among a##tag's properties (## dashboard { columns=2 } artifact { … }), as the server does. The builder opens such a file read-only, with the reason, untilartifact { … }is moved first on the tag.- The builder's file reader works when a host's bundler turns the CommonJS
@malloydata/malloyimport into a default-only module (Vite's dev server did, and the reader threwn is not a constructor). - The same holds for
@malloydata/malloy-tagand@malloydata/malloy-query-builder: the builder, the chart-theme reader and the model explorer no longer break when a host's bundler hands back a default-only module for them. - The builder's Save caption is the host workspace's own
descriptionwhen it supplies one (for example "Saved to this draft"); with none, a host save reads "Saves to the app this is embedded in", where it used to say "Saves where the host app keeps it". The description is said once, as the Save caption, and is no longer repeated under the tiles. A save into the package keeps its caption. NotebookEditortakespath, asDashboardEditordoes.- The builder reads a CRLF file as it reads the same file with LF endings; a
##|"description block used to be dropped. Saving one writes CRLF throughout, where the lines it inserted or rewrote used to come out LF. - Save and exit in the leave prompt no longer leaves an Undo save offer, since the editor closes.
- The read view's
DashboardViewtakes achromeprop, and a narrowed tile now narrows instead of keeping the width of the chart it replaced.
The package page has a New menu with Dashboard and Notebook: choose a type (Dashboard or
Notebook), a model, a source and its view (one select), and a title, and the file is created and opened in its editor. On the Console it creates the
file in the package and refuses to overwrite one that exists; on a host with an authoritative record
it creates the document there. The menu is offered when the server takes writes or a host keeps the
record and can store; it is not offered when neither route exists, on a record that cannot store, or
on a pinned version of a package. The same primitives (createDocument, createRoute, useDocumentChoices,
newNotebookSource, locatorFor) are exported from @malloy-publisher/sdk/builder, and
dashboard.created is a new case on DashboardEvent, and NotebookEvent is a new event type
that carries notebook.created (both { where } only; additive, but a host that switches
exhaustively over DashboardEvent will see one new case).
A dashboard tile gets a Viz type picker: From the view (the view's own chart), Table, Line, Bar,
Big value, Scatter, Shape map and Segment map. Every choice is listed; one the view cannot render is
disabled with its reason beside it (Big value needs a view with only totals, a map needs a view that
already carries a map chart). The picker writes one chart
line that turns off the chart tags the view carries (all the others when the catalog does not know the view). A chart line it did not write (for example
# bar_chart { size=spark }) is kept byte for byte on every edit, and the picker is disabled for
that tile with the reason shown.
A dashboard with no tiles now opens in the editor instead of being refused. It is not served until it
has a tile (its page 404s and the load lint reports it), and the editor will not remove the last tile.
Fixed: a filter on a joined dimension. A dashboard filter added in the builder on a dimension
reached through a join (products.category) was written with only its last segment, so its options
came from the wrong field. It now keeps the full path, always quoted in the file. The dashboard lint
now accepts one level of join in a control's suggest { dimension=… }, where it used to warn that
the source had no such field.
The dashboard and notebook builders ask before discarding edits, and say why a control is off
Leaving with unsaved edits now asks. "Close" in DashboardBuilder and DashboardEditor (and NotebookEditor, which wraps them) shows a "Leave with unsaved changes?" prompt when there are edits the record does not have: Keep editing, Discard changes, or Save and exit (Save and exit is absent where nothing can be written, such as a read-only host or a pinned version). An open text draft counts as an edit. In the Console, the dashboard and notebook edit pages also block Back, a link to another page, and closing the tab while dirty; the Console prompt offers Keep editing or Discard changes only, and Back used to leave without asking.
For hosts that mount an editor themselves, the builders now draw "Close" and take two props: onExit, called once the user has chosen to leave, and onDirtyChange(dirty), the hook for your own navigation guard. A host that passed its own exit button as toolbar should pass onExit instead, and a host whose onExit also prompts will now prompt twice. UnsavedChangesDialog is exported from @malloy-publisher/sdk for pages that need the same prompt.
Smaller changes in the same pass:
- Text tiles have a Cancel button that drops the draft; Escape, Done or Cmd/Ctrl+Enter keeps it, and Cmd/Ctrl+S saves from inside the field.
- Disabled controls say why. Remove tile on a saved dashboard's last tile, and a Viz type choice the view cannot render, stay focusable and show their reason on screen.
- One create entry. The package page's header New menu is the only create button; an empty Dashboards or Notebooks section offers "New dashboard" or "New notebook" in its own row.
- Small screens. Below 600px the Console hides Edit and New, and an edit page opened there says editing works best on a larger screen, with Edit anyway.
#(authorize) mentioned in markdown prose is no longer refused
Query, compile and write text that carries #(authorize) or #(access_filter) is still refused,
except where the tag sits inside the body of a (markdown) or (text) block note, or after the
(markdown) or (text) prefix of a line note: there it is prose, so a notebook that writes about a
gate can be previewed and saved. The same tag anywhere else, including a line note with any other
route, a block's opener or closer line, a comment or a string, is refused as before, and text the
server cannot lex is judged the old way.
A query that reads a given with no default no longer keeps its package from loading
A model, notebook, or dashboard whose query reads a given that has no default (every #(access_filter) given, by rule, and any given: ORG :: number a run: filters on) failed to compile while the package loaded, and one failing file aborted the whole load: the package was missing from its environment. A reload failed the same way but kept serving the model compiled before it, marked stale, so the package went missing only on a first load or restart. Such a package now loads, and the query is listed. Running it without a value for the given is refused exactly as before; with one it returns rows.
One case still fails to load, with or without a default: a source view: whose later nest: stage reads a given. That is a Malloy limit, not specific to this change.
What's Changed
- skills: make the skills readable and installable with nothing running by @jswir in #1169
- refactor(get_context): split runContextQuery into stages, pin its payloads, and order exact ties by @jswir in #1298
- feat(embedding): build the semantic index when a package loads, save each batch, and retry transient failures by @jswir in #1299
- feat(get_context): answer indexing or error instead of a lexical ranking when embeddings are configured, and search a source's own fields by @jswir in #1300
- feat(server): add the provider layer and retrieval settings for chat and embedding models, the call ceiling and the retrieval trace by @jswir in #1301
- feat(get_context): add keyphrases, source summaries, refine, rerank and source matching with an LLM by @jswir in #1302
- test(server): end embedding cool-downs explicitly instead of racing the clock by @sagarswamirao in #1307
- chore(deps): upgrade @malloydata/* to 0.0.435, so a failed Postgres query closes its session and Trino map and json columns return values by @sagarswamirao in #1306
- ci(docker): sign the published image with cosign and verify the signature before the release completes by @sagarswamirao in #1309
- fix(deps): clear new CRITICALs in simple-git, seroval and proxy-addr by @housejester in #1314
- feat(server): return the document from compile, authorize it per cell and tile, and edit it as text in the SDK builder by @Sha-Bang in #1311
- feat(console): a calmer dashboard and notebook builder, and one theme accent by @kylenesbit in #1316
- ci(docker): pin cosign-installer to v4.1.2 (there is no v4 tag) by @kylenesbit in #1317
Full Changelog: v0.9.2...v0.9.4