Repository navigation
v0.9.8
Release v0.9.8
📦 NPM packages
🐳 Docker image
docker pull ms2data/malloy-publisher:0.9.8
docker pull ms2data/malloy-publisher:latest
ℹ️ Details
- Release branch:
release/v0.9.8 - Triggered by: @kylenesbit
Reports: one document type, anywhere in a package, with a layout
Dashboards and notebooks are now one document type, the report, and a report can live anywhere in a package. A report is any .malloy file outside public/ that carries a model-level ## artifact tag, other than the root index.malloy; a .malloynb is a linear report wherever it is. dashboards/ and notebooks/ are now ordinary directory names: nothing about a file depends on which directory holds it, and a report two directories deep is served like one at the root. Every file served from those two directories before is classified the same way by its tags alone, with one exception, listed under "What may change" below.
A report's layout is grid or linear, decided by its tag: layout=grid / layout=linear; else grid when the tag lists tiles or the file carries a model-level dashboard { … } grid tag; else linear. One reader decides it, over the file's model-level notes, whether the file compiled, failed to, or is text handed to the compile check or the save, so every one of them answers the same.
A report is identified by its path. Two reports may share a basename in different directories and both are served; the old shadowing (the second file was not served) and the write path's name-collision 409 are gone.
New routes. GET …/reports lists every report with path, name (the basename, a label), layout, title, description, editable and any load error. GET …/reports/{path} returns one report by its full package path including .malloy (or .malloynb), exactly as the listing gives it: the manifest fields for a report written as tiles, or notebook (its cells) for a run:-cell report or a .malloynb. There is no report cell route: a .malloy report's run: cells run through POST …/models/{path}/query, each cell's text as the query, the governed endpoint grid tiles already use. DashboardManifest, RawNotebook.dashboard and a compile's CompiledDocument gain layout; package warnings gain an optional code. The report schemas reference nothing deprecated: ReportTile and ReportCells are the tile and cell shapes, with DashboardTile and RawNotebook as deprecated aliases. A write whose rollback itself fails answers 500. Severities are unchanged: every new finding is warn or error, so a client that accepts only those keeps working.
Writing. PUT …/models/{path} (the builder's save) now creates a report at any .malloy path outside public/ other than the root index.malloy (refused whatever its case), creating missing directories inside the package root only, and replaces only a file the package reads as a report. It still cannot overwrite a model, an untagged file, a .malloynb or index.malloy. Text whose only ## artifact is inside a comment or a string is refused with 400 up front, and a link out of the package is refused before anything is read through it. See docs/security-posture.md.
SDK. createDocument (and the New report dialog) writes a new report at the package root by default, so CreatedDocument.path is sales.malloy where it was dashboards/sales.malloy or notebooks/sales.malloy, and the template always writes layout=grid or layout=linear in the tag. A host whose DocumentStorage files documents somewhere other than the path asked for (every grid report under dashboards/, say) implements the new optional resolveDocumentPath(locator), returning the package-relative path saveDocument will really write: the new file's imports are rendered for that path and CreatedDocument.path is that path. A store without it is assumed to write where asked; on such a store that relocates files, a new report's imports would not resolve. Removed exports: documentPathFor(kind, slug) and documentPathForTitle(kind, title, suffix?), which named dashboards/<slug>.malloy / notebooks/<slug>.malloy, are gone from @malloy-publisher/sdk/builder. Use reportPathFor(slug, directory?) and reportPathForTitle(title, suffix?, directory?), which name a report's file at the package root (or in directory); a host that files reports by kind builds its own path. createDocument checks every path it computes, a directory included, so directory: "../x" or "public" is refused before the package or the store is asked anything. ReportEditor and ReportBuilder take allowLayoutChange (default true); false hides the Linear / Grid switch, for a host that files reports by layout. The Console's builder route is now the report's route with ?edit (/<env>/<pkg>/sales/q3?edit), so a report whose path ends in edit opens as itself; reportEditRoute(env, pkg, path) builds it, and the package page's onOpenDocument default and drafts use it. A …/sales/q3/edit link still opens the builder by redirecting to ?edit, unless a report is at sales/q3/edit.malloy. A report under data-apps/ opens as a report rather than as a data app. The builder reads a file's layout as the server does, from its model-level notes alone, never its folder: layout=, else grid when the tag lists tiles or the file carries a model-level dashboard { … } tag, else linear; a tag that sets kind= opens nothing, and the builder names the layout= to write. readReportTagOfText(text) (@malloy-publisher/sdk/builder) gives that answer for any text.
SDK changes a host must make (all to APIs the new format introduced or to the storage contract):
DocumentStorage: a report's path is its identity.DocumentLocator.path(<env>/<pkg>/<package path>) is the key: one path is one document.DocumentLocator.type(DocumentType) is now the report's layout,"grid"or"linear", metadata a save records and a listing reports, where it was"dashboard"or"notebook"and part of the key. Migration for a host store: key documents by path alone, recordtypeon save and move, report it fromlistDocuments, and read"grid"for an old"dashboard"and"linear"for an old"notebook".BrowserDocumentStoragemoves drafts saved under its old type-keyed scheme to their paths the first time it is used, so none is lost.useDraftTypeis nowuseDraftLayout, answering{ layout, settled }.- The builder's document carries
layout(DashboardDocument.layout,"grid"or"linear") in place ofkind. Migration: readdocument.layout === "linear"where you readdocument.kind === "notebook".SpliceOptionsloseschangeKind,modelPathandexplicitKind, andReportBuilderlosesexplicitKind: none changed anything any more. ReportEditortakeslayout, the layout a report opens with (from the listing, or a draft's recorded layout);kindstill works, deprecated. Once open, the document's own layout decides how controls are drawn, which layout a save records, and which events the builder reports, so they follow a layout switch.createDocumenttakeslayoutonly: its deprecatedkindoption,CreatedDocument.kindandOpenDocumentRequest.kindare gone, andnewDocumentProblem(input)takes no kind.LocatedDocumenthaslayoutand nokind. Migration: readlayout.- Drill resolves paths only.
resolveDrillTargetanswers{ kind: "path", report }or{ kind: "unresolved" }; the by-name arm andcandidatesare gone, and the Console never builds a/dashboards/<name>link for a drill.locateReportByName,locateDocumentanddocumentRouteremain, deprecated, for old links only. Reportrenders.malloyreports; a.malloynbis rendered by the deprecatedNotebook, the only component that calls the deprecated notebook routes. The Console routes a.malloynbthere.reportRouteencodes the environment and package names as well as the path, so no caller encodes them again.
Server telemetry, renamed outright (no dual emission). Dashboards to update:
publisher_dashboard_writes_total→publisher_report_writes_total, andpublisher_dashboard_write_duration_ms→publisher_report_write_duration_ms. Thekindlabel is nowlayout(grid/linear);outcomekeeps its values and gainsrollback_failed(a write whose restore failed too, so the package may serve either text); the counter gainsreason, why a write did not land (frozen_config,invalid_path,missing_source,untagged,not_a_report,conflict,compile_failed,not_found,reload_failed,bad_request,other;nonewhen it did).publisher_notebook_discovery_totalis removed. Discovery is now counted for reports of both layouts:publisher_reports_discovered_total{layout},publisher_report_files_total{status}(served,tileless,invalid,dropped) andpublisher_report_lint_findings_total{code,severity}, once per load or reload, withpublisher_report_discovery_duration_ms{during}(load,reload,compile) timing every pass. A notebook whose cells the reader refused, its oldoutcome="refused", ispublisher_report_lint_findings_total{code="notebook-cells-unreadable"}; a.malloynbload ispublisher_deprecated_usage_total{feature="malloynb"}.- New:
publisher_report_read_duration_ms{route,status_class}forGET …/reports(list) andGET …/reports/{path}(get), andpublisher_deprecated_usage_total{feature}, one per use of a deprecated feature: the legacy routes (dashboards_list,dashboard_get,notebooks_list,notebook_get,notebook_cell), the Console's/dashboards/<name>alias (console_alias), and per load or reload a.malloynbfile (malloynb), a package settingexploresorqueryableSources(explores,queryable_sources), a source declaring#(filter)(filter_annotation), and a query sending filter values or a bypass (filter_params,bypass_filters). It is the evidence for when each can go. publisher_notebook_cell_executions_totalandpublisher_notebook_cell_execution_duration_msare unchanged; they measure the deprecated cell route only.
Logs: report lint findings are no longer logged at warn on every load, reload and metadata change. Each load or reload logs one info line, Report discovery, with the package, during, the counts by layout and status, and the findings by code; each finding is a debug line, Report lint finding, with code, severity and model. Report discovery and write log lines name the file model (was modelPath) and the failure error (was detail). A rolled-back write is logged once (Report write rolled back, warn), and a failed rollback once at error (Report write rollback failed) with both error and restoreError; a refused write is Report write refused at warn. A 4xx on the report and legacy report routes is logged at warn, not error.
SDK telemetry, renamed outright (no dual emission). Every report surface reports one event family, ReportEvent, through the onEvent props it already had, and every event carries the report's package path and its layout (grid/linear; absent only on a report.load_failed that happened before the report said). The host adds the environment and package when it logs, as before.
- Removed SDK types:
DashboardEvent,DashboardEventHandler,NotebookEvent,NotebookEventHandler,BuilderEvent,DocumentCreatedEvent,DashboardCreatedEventandNotebookCreatedEvent. UseReportEventandReportEventHandler(both entries), withReportEventContext,ReportFailure,DrillOutcomeandSavesTo. - Renamed events:
dashboard.opened/notebook.opened→report.opened(surface: "edit";cellsis nowtiles);dashboard.open_refused/notebook.open_refused→report.load_failedwith acategory(not_found,forbidden,invalid,unsupported,server,network,unknown) and noreason, since the message can carry a path or a value;dashboard.saved/notebook.saved→report.saved(cells→tiles;convertedis present only when true);*.save_refused,*.save_undone,*.save_undo_refused→report.save_refused,report.save_undone,report.save_undo_refused;dashboard.created/notebook.created→report.created(now withdurationMs);dashboard.rows_shown→report.rows_shown;dashboard.explored→report.explored. Migration: match on thereport.*name and readlayoutwhere you read thedashboard./notebook.prefix. - New events:
report.openedandreport.load_failedfromReportitself (surface: "view"), once per report shown, for grid, linear andrun:-cell reports alike, where before only a grid report's rows dialog reported anything;report.query_failed(tileindex,category) for a tile or cell whose query failed;report.create_refused(reason) for a create that did not land;report.layout_changed(from,to) when an author switches Linear/Grid;report.drill(to: "self" | "report",outcome: "resolved" | "unresolved" | "malformed"). - Prop changes:
Report'sonNavigatemay return aDrillOutcome, whichReportreports asreport.drill; returning nothing reports nothing.PackagetakesonEventand passes it to its New report dialog.DashboardViewno longer takesonEvent: it reports through theReport(or deprecatedDashboard) that renders it. Inside aReport, a failed cell and a refusedto=selfdrill go toonEventand are no longer logged to the browser console; without anonEventthey still are. - Console: every event is one log line under
[publisher.report](was[publisher.dashboard]and[publisher.notebook]), atwarnfor a failure (load_failed,query_failed, any*_refused, a failedrows_shown, a drill that did not resolve) andinfootherwise. - A host with an event sink must update it before taking this SDK. Credible's draft editor matches
dashboard.saved,notebook.saved,dashboard.save_undone,notebook.save_undone,dashboard.createdandnotebook.created, and importsBuilderEvent; it needsreport.saved,report.save_undoneandreport.created, andReportEvent.
Deprecated, still working until a later release removes them:
-
GET …/dashboardsand…/dashboards/{name}(the grid reports) andGET …/notebooks,…/notebooks/{path}and…/cells/{i}(the linear reports).…/dashboards/{name}resolves a basename only when exactly one grid report has it, and answers 409 naming every match when several do. Move to…/reports.These deprecated routes still answer, but their contracts are wider than before, and a client of them should check:
GET …/dashboards/{name}can now answer 409 (several grid reports share the basename). It never did before. The package warns about each such basename at load (report-name-ambiguous,warn, naming the paths); fix it by renaming one of the files.GET …/dashboardsandGET …/notebooksnow list reports from anywhere in the package, not only files directly underdashboards/ornotebooks/: apathmay besales/q3.malloyora/b/c.malloy, and a grid report innotebooks/is listed by…/dashboards, a linear one indashboards/by…/notebooks.Dashboard.nameis still the basename, but it is no longer unique in the listing; key onpath.- The SDK's
DrillNavigation.path(what a# drillclick hands a host;dashboardbefore, see below) is the tag'sto=, which is now usually a path containing/(sales/overview) rather than a single-segment slug. Resolve it withresolveDrillTargetagainstGET …/reportsrather than putting it into a URL as one segment.
-
SDK names that said "dashboard" for a report, renamed with the old names kept as deprecated aliases, removed with the other deprecated APIs. Migration: import the new name; nothing else changes.
DashboardEditor→ReportEditorandDashboardBuilder→ReportBuilder(@malloy-publisher/sdk/builder), withDashboardEditorProps→ReportEditorPropsandDashboardBuilderProps→ReportBuilderProps. The old names are the same components and types.DrillNavigation.dashboard→DrillNavigation.path. Both are set, to the same value, on every drill.
Removed, not deprecated. These existed only for the new format, which has no users yet, so they go now rather than through a deprecation window:
kind=in an artifact tag is no longer read. A tag that sets it makes no report, and the package reportsreport-kind-removed(error). Migration: writelayout=gridforkind=dashboard,layout=linearforkind=notebook, and dropkind=.- The query-level
# artifacton aquery:is no longer read; only a model-level## artifactmakes a report, and a file whose only tag is on a query is an ordinary model.query-artifact-deprecated,query-artifact-ignoredand the manifests'queryfield go with it. Migration:## artifact { layout=grid <the same properties> tiles=["<query>"] }at model level, with the query's# dashboard { … }left on the query. - A
# drill { to=… }target is always a path from the package root; a bare name is a root file. The basename fallback anddrill-target-by-nameare gone. Migration: write the report's path,to="sales/overview"forsales/overview.malloy.
What may change in an existing package. A model outside dashboards/ and notebooks/ that carries an artifact tag is now a report: it leaves the model listing on a curated package and contributes nothing to the surface. The package says so with report-on-surface whenever index.malloy imports it or explores lists it. A tagged root index.malloy stays the surface, and its tag is ignored with an index-is-not-a-report error. A kindless, tileless ## artifact in dashboards/ (an unserved empty dashboard before) is now a linear report, reported as report-layout-implicit. A notebooks/x.malloy whose ## artifact lists tiles and names no kind= or layout= read as a notebook before, because the folder supplied the kind; it is now a grid report, so it moves from …/notebooks to …/dashboards and renders as a grid. Nothing warns about it: add layout=linear to keep such a file linear. A file whose artifact tag sets kind=, or whose only tag is a query-level # artifact, is no longer a report (see Removed above). report-on-surface follows index.malloy's imports transitively, as the surface does. notebook-other-folder and notebook-kind-missing are retired, and a package's own public/dashboards/ and public/notebooks/ files are reachable at /<env>/<pkg>/dashboards/<file> again.
What's Changed
- chore(release): 0.9.7 by @housejester in #1372
- feat(server): give the deprecated-format package warnings a stable code by @kylenesbit in #1371
- feat: reports — dashboards and notebooks as one document type, anywhere in a package by @kylenesbit in #1373
- test: move tests, fixtures and examples off deprecated features by @kylenesbit in #1376
- refactor: reports quality pass — simpler code, isolated deprecations, test pyramid, telemetry by @kylenesbit in #1377
Full Changelog: v0.9.7...v0.9.8