@masterteam/dashboard-builder
v0.0.86
Published
Dashboard Builder components and services for Angular 21+ with Tailwind CSS 4. The package supports authoring, saving, loading, and rendering dashboards with charts, header widgets, topbar widgets, metadata-driven data sources, source links, and respon
Downloads
3,015
Readme
@masterteam/dashboard-builder
Dashboard Builder components and services for Angular 21+ with Tailwind CSS 4. The package supports authoring, saving, loading, and rendering dashboards with charts, header widgets, topbar widgets, metadata-driven data sources, source links, and responsive ECharts output.
Features
- Multilingual dashboard item titles stored as
clientConfig.title.en/ar. - Header, topbar, and chart item title editing in builder mode.
- Host-interceptor-owned save messages, with automatic saves marked silent.
- Registry-driven chart query schemas, including safe preview states for incomplete draft charts.
- Stacked-bar query fields switch between auto-stack and manual-stack modes so only the relevant stack property selector is displayed and emitted.
- Quick Manage style changes stay local in the drawer until Apply, with an isolated Preview tab for checking the edited chart config before the dashboard canvas is updated.
- Chart header icons provide preset-based appearance and placement controls, including independent standalone/group visibility and grouped icon-only mode.
- Timeline multi-level queries persist per-level
extraPropertiesand still read legacyextrasfields. Open timeline detail popovers close when the page, dashboard, or timeline scrolls outside the overlay, while the detail panel's own scrolling remains available. - PhaseGate Table uses a dedicated semantic-role editor and sends every configured runtime property in one deduplicated backend projection.
- Ring Gauge with Formula accepts either
valuePropertyorformulafor each series, exposes no Group By or Extra Properties controls, and renders legacyvalues[].bars[]responses through the concentric-ring gauge adapter. It keeps each numeric detail visible (including0) without exposing the generated series key as a gauge label or legend entry. - Every GeneralQuery capability that includes
formulaexposes a visible per-series Show/Hide advanced settings action and a Formula Builder scoped to that series; formula availability is schema-driven rather than chart-specific. It uses the legacy P+4 Dashboard Builder dialect rather than Formula Engine: the original nine function keys and eight punctuation operators compose a compact expression without inter-token whitespace, and selected properties serialize as bare keys such asactual_progress(for example,sum(actual_progress), neversum ( @Current::actual_progress )). Existing scoped or spaced chart formulas are migrated on load, save, and execution; whitespace inside quoted custom values is preserved. - Selected-property chips in dashboard query editors can be removed inline.
- Properties/card-list items normalize null Percentage values to
0with an empty progress track and never inherit a sibling card's runtime percentage. - Duplicated charts receive a new local dashboard identity, omit the source
link/component IDs so the next save creates a new backend chart, and persist
duplicatedFromas provenance to the source dashboard identity. - Workspace-aware metadata loading for modules and properties.
- A dedicated chart Filters action separates viewer-facing filter controls and
built-in conditions from datasource configuration, using shared
mt-*form fields throughout the condition editor. - Navigation actions expose a
Navigation Targetselector. Both page and entity navigation default to the current tab and can explicitly open in a new tab throughaction.config.navigationTarget. Page navigation replaces only the active dashboard id while preserving host route segments such asmanageordashboards. Entity navigation uses browser-level application navigation so a current-tab action triggered in the control-panel builder can load the app-client workspace route instead of falling through the admin router to All Workspaces. - Progress Update cards render activity HTML through ngx-quill's read-only renderer, preserving the rich-text size, color, font, alignment, direction, and semantic formatting configured in the source editor.
- Persisted dashboards can be deleted from the page-settings drawer after a named, danger-styled confirmation; successful deletion returns the host to its dashboard list while standalone builders keep deletion unavailable.
- Module multi-select serialized as selectors such as
Level:1,2,3. - Source-link chaining with previous result fields, relationship fields, and the
Parentoption exposed to child-level configuration. - Distinct aggregation normalization from legacy
DisticntCounttoDistinctCount. - Container-aware ECharts option mapping for compact dashboard grid cells,
including shared legend placement for donut, pie, bar, line, stacked bar, and
other ECharts-based charts. Pie/donut radius, center, and slice-label geometry
stay handler-owned to match the old dashboard builder; the shared EChart
wrapper reserves the same chart area as the old wrapper and does not apply
pie-specific responsive overrides. Native ECharts legend rendering stays
authoritative; the wrapper maps legend dimensions before grid offsets and
clamps saved percentage grid values against the current card pixels so stacked
bar legends keep reserved space after resize. Donut slice labels never repeat
the category name — that stays in the legend and the ring carries the value —
and they are shown or hidden by the
Show labelstoggle (configAsType.label.show), not by whether a legend is visible. Pie legend vertical alignment applies only to left/right legends, so old saved vertical-align values do not move the default bottom legend into the chart body. Compact bar and line charts preserve their configured value-label visibility instead of hiding labels when the card height crosses a responsive breakpoint.
Installation
pnpm add @masterteam/dashboard-builderUsage
import { Component } from "@angular/core";
import { DashboardBuilder } from "@masterteam/dashboard-builder";
@Component({
imports: [DashboardBuilder],
template: ` <mt-dashboard-builder [services]="['pplus', 'splus']" (onSave)="saveDashboard()" /> `,
})
export class MyComponent {
saveDashboard(): void {
// React to a completed explicit save.
}
}Metadata Contract
Hosts must provide the dashboard metadata endpoints used by
DashboardBuilderService:
GET metadata/workspacesreturns workspace options:WorkspaceOption { id: string | number; name: string }.GET metadata/modules/treeacceptsservices,workspaceId, andincludeValues. Returned module groups, items, and values may includeworkspaceIdfor client-side filtering.POST metadata/modules/propertiesacceptsitemsand optionalworkspaceId. Each request item may also includeworkspaceId.GET Modules?LevelId={levelId}backs the Status filter's Log select. It returns the product module catalog withisActivemarking which modules are attached to that level; the builder keeps the active entries whoseclassificationisLog(the enum name or the ordinal0). Hosts that already own this list can override it with[levelLogs]onManageFilterOnPage/DynamicFiltersConfig.
When a workspace changes, the builder clears selections and source links that no longer belong to the selected workspace.
Table Query Editing Contract
The generic Table query editor persists ordered selectedProperties,
PivotProperties, AggregationProperties, and an optional lookupProperty.
Its ControlValueAccessor treats writeValue as silent model-to-view
synchronization: an equivalent value is ignored, and aggregation controls are
rebuilt with emitEvent: false. This is required because DataSourceSettings
immediately feeds each ngModelChange back into the editor; emitting during
that echo creates a synchronous loop that freezes the Properties picker
(#1284).
Snapshot Query Contract
Snapshot charts distinguish the selected Level schema from the concrete Level record whose history is requested:
interface SnapshotQuery {
workspaceSelector?: string | null;
levelId: number; // schema id used to load record options
levelDataId: number; // record id sent to GetSnapShotInfo
snapshotTargetVersion: 2; // prevents schema ids from using the legacy fallback
timeFrame: "yearly" | "monthly" | "weekly";
year: number | "{{currentYear}}";
getAveragePerPeriod?: boolean;
selectedProperties: string[];
}After a Level is selected, the builder requests its records through
POST fetch/query with:
{
contextKey: `level:${levelId}`;
projection: "Card";
display: {
areas: ["card"];
}
}This authoring request deliberately omits includeState. Record options map
directly from data.records[].id/name. Runtime calls
POST dashboards/{apiVersion}GetSnapShotInfo/{levelDataId}/{timeFrame}?year={year}
with { AvgPerPeriod, dashboardId }; the schema levelId must not be used in
that URL. Supported selected-property
keys are plannedProgress, actualProgress, budget, baselineProgress,
spent, remaining, and paymentPlan.
Axis Label Wrapping Contract
Bar and stacked-bar category labels preserve their original casing and wrap
normal spaces, underscores, existing line breaks, and single tokens that exceed
the selected character limit. Runtime keeps all generated lines visible and
sets hideOverlap: false; the shared ECharts responsive layout still owns the
available label width and grid containment.
The persisted controls live under clientConfig.barConfigOverride and
clientConfig.stackBarConfigOverride:
{
axisLabelWrapMode: "characters" | "words" | "none";
maxCharsPerLine?: 8 | 12 | 16 | 20 | 24 | 30 | 40;
maxWordsPerLine?: 1 | 2 | 3 | 4 | 5 | 6;
}Missing mode values use character wrapping, so old dashboards that only saved
maxCharsPerLine remain compatible. The default is 20 characters per line;
word mode defaults to three words per line. none normalizes separators but
keeps the label on one line.
Host Currency Settings Contract
Dashboard currency labels, tooltips, and values read the host's canonical
systemSettings.settings local-storage entry. The stored value must be a
JSON-encoded array containing a case-sensitive Currency key whose value
provides the currency code, localized units, and numeric format:
localStorage.setItem(
"systemSettings.settings",
JSON.stringify([
{
key: "Currency",
value: {
name: "OMR",
unit: {
en: "OMR",
ar: "ريال عماني",
},
format: "{0:N3}",
},
},
]),
);Runtime currency precedence is:
- The valid
Currencyentry insystemSettings.settings. - The legacy JSON object stored under
currency(itsunitstring). - The built-in SAR unit and legacy number formatting.
Arabic dashboards (including regional language codes such as ar-SA) use
value.unit.ar; every other language uses value.unit.en. Supported system
formats include decimal/grouping patterns such as {0:#,0.00}, compact
thousands such as {0:#,0, K}, and named decimal patterns such as {0:N3}.
The host currency format is authoritative; a chart's legacy decimal option does
not override its decimal count.
The showcase's opt-in offline dashboard fixture
(showcase-mock-dashboard-builder=1) appends the OMR example above only when
the settings array has no Currency entry, so an existing host choice remains
authoritative.
PhaseGate Table Query Contract
2-table-view does not use the generic table pivot/aggregation query. Its
canonical service query is:
{
rowIdentityProperty: string;
phaseNameProperty: string;
phaseIdProperty: string;
phaseOrderProperty: string;
phaseStatusProperty: string;
phaseProgressProperty: string;
phaseIsCurrentProperty: string;
phaseIsCompletedProperty: string;
phaseAllItemsCompletedProperty: string;
baseProperties: string[];
properties: string[];
selectedProperties: string[];
}baseProperties is the ordered multi-select of visible business columns. Both
properties and selectedProperties are rebuilt before save/request as the
same deduplicated union of those columns and every non-empty semantic role, so
the chart service receives all fields needed by the renderer.
The runtime resolves configured roles by property identity, matching the row
object key or wrapper key, normalizedKey, label, or name without
depending on response order. Consequently a visible project Status and the
phase Status can coexist safely. Dynamic phase headers use the configured
phase property's runtime value rather than its schema/metadata label, and an
empty phaseProgressProperty does not render a zero-value progress track.
Legacy generic-table configs are still read;
lookupProperty becomes the phase-status role, with conservative runtime
fallbacks until the remaining roles are configured and saved.
Runtime Filter Contract
Dashboard filters have two independent scopes:
Page/general filter definitions live in the dashboard configuration's
filterscollection. The viewer supplies their applied values to every item; request templates consume the keys they reference.ignoreQueryFiltersuppresses ambient global query values but retains explicit viewer/dialog filter context for compatibility with saved pages. The viewer opens a default-closed shared MT drawer from its in-flow Manage Filters action, so the host does not need a fixed sidebar or overlay CSS. Apply commits the merged values to the runtime store synchronously, then mirrors them to the URL as the shareable source of truth. Every mounted item observes the forwarded query- param signal and requests data again when it changes, so Apply, Clear, browser navigation, and externally-authored query params update the dashboard without a page reload. The later route echo has the same serialized state and does not duplicate the request.Chart filter definitions live canonically in
clientConfig.dynamicFilters. LegacyclientConfig.extraFilters.dynamicFiltersis read and migrated when the chart is edited. Runtime values are stored per widget identity and override a page value with the same key for that widget only.Viewer
extraFiltersare host-owned context (for example, an informative workspace'scurrentId). They are merged after page and chart values, so a same-key runtime value cannot replace or clear the host's scope.Builder and viewer hosts pass the same client-form metadata through
[lookups],[statuses], and[levelsSchema]. Lookup configuration labels are normalized from the host lookup names, runtime Status options accept canonical nested status definitions or legacy flat level statuses, and Status/Phase Gate schema configuration is derived from the shared hierarchical metadata tree'sLevelmodule.[levelsSchema]is the host's levels collection; runtime Phase Gate and Schema Settings options read it, and it replaces the legacypplus.settingslocalStorage cache for hosts that never wrote one.A Status filter's "Log" options are scoped to the row's selected level, so two Status filters on different levels never share one list. They resolve in this order:
The host
[levelLogsResolver], when one is registered. Hosts that already reach the module catalog through their own authenticated client should supply one — it removes every base-URL and permission unknown from the package's side. Without it the package requestsGET Modules?LevelId={levelId}itself. Either way the result is cached per level for the session, and a failed load is reported throughconsole.warnso a 401/403/404 is never mistaken for "no logs".Note that endpoint returns the WHOLE module catalog and marks the level's own modules with
isActiverather than filtering, so a level's log isisActive === trueandisLogClassification(classification)(ModuleClassificationEnum.Log;Processis not a log). That endpoint serializes the enum as its name ("Log"), while others emit the ordinal0— compare throughisLogClassification, never a bare=== 0, which silently drops every module and empties the dropdown.The level's own
logscollection, when the host levels schema carries one (the legacy PPlus4 settings contract).Status metadata: every definition scoped to that level that names a
moduleId. This derivation is the exact inverse of the runtime read (getStatuses(logId, 'Log')matches onmoduleId), so every offered option resolves to real status values. Labels come from the metadata tree'sModulevalues, falling back to the status property name.
Sources 2 and 3 need no request, so the select stays populated for hosts that cannot reach the
Modulesendpoint.
Both drawers edit a private draft. Apply commits and refreshes the relevant
scope; closing without Apply discards the draft. Clear replaces only the keys
owned by that drawer's field set: optional values are removed, required
non-date values remain selected, and static chart conditions plus the other
runtime scope are preserved. false and 0 are valid applied values rather
than empty values. Drawer ownership follows the viewer/widget lifecycle, so an
open body-appended drawer is closed when its owner is destroyed and cannot
commit into a stale chart store.
Compatibility key mapping follows the original dashboard builder while keeping each scope's natural runtime types:
- page/query values serialize multi-selects as comma-delimited strings, users
as the id under
keyplus the display name under<key>Name, checkboxes as"true"/"false", and quarters under<key>Splus<key>E; - chart item-store values keep multi-selects as arrays, checkboxes as booleans,
users as
{ userId, displayName }objects (or arrays of those objects), and quarters as the combinedstart,endvalue under the base key; - Clear owns every compatibility key for its field: users own
keyand<key>Name, while quarters ownkey,<key>S, and<key>E.
Request substitution accepts both representations. Page values are merged first and a chart's typed item-store values win only for that widget.
The parent report id (pageId) is the request execution context. A chart's
serviceConfig.dashboardId remains its widget/store/cache identity and must not
replace the report id. Route query parameters, including a legitimate filter
named id, are never used as the report identity.
Dialog actions retain that report id and expose the merged page/chart context
both under parentFilter and as flat keys. This keeps current
{{parentFilter.year}} templates and legacy {{year}} templates compatible.
Compatibility Notes
- Saved selectors remain backward-compatible with legacy
moduleType/moduleIdvalues and comma-based module ids. - Existing multilingual title objects are preserved. Plain string titles are normalized to the active title object shape.
- Removing a persisted chart group unlinks its saved group container before clearing the local group metadata. Its child charts remain on the dashboard and the deleted empty group cannot return after Save and reload.
- Style Studio resolves Header and Topbar widgets without a
chartTypeIdthrough their registeredclientConfig.componentName. This selects their layout-specific controls (without chart legend, label, palette, or number formatting options) and previews draft styles before Apply. Cancel continues to discard the working draft. - Style Studio preview/runtime parity includes Header and Topbar icon placement,
bar and stacked-bar widths,
configured number precision and explicit currency visibility, pie legend
type/font controls, bilingual donut center labels, table/timeline legend
position and icon shape, and statistic-card icon placement. Stacked-bar
totals are emitted only when
configAsType.label.showTotalInTopis true; the generic data-label toggle cannot override that dedicated setting. - Performance Gauge Card treats the
Statuswrapper returned invalues.*.extraValuesas authoritative for the active status key, localized label, and color. Component status ranges still define the gauge scale and segments, and remain the fallback when the response has no status wrapper. - The builder does not show save toasts directly. Save response messages belong
to the host HTTP interceptor; automatic/internal save requests are sent with
noMessage: true. - Aggregation requests normalize legacy
DisticntCountvalues toDistinctCount; backends should accept both names and execute distinct-count semantics. - Table-card status legends support both grouped and flat label payloads,
display legacy
labeltext alongside the color indicator, and expose visibility plus per-entry order/color controls in Style Studio for Table, Dialog Table, and PhaseGate Table items. - Table and Dialog Table user cells resolve the canonical
photoUrlvalue before the legacyimageandavataraliases, then load the avatar through the shared authenticatedsecureImagepipeline instead of binding the backend value directly to<img src>. - Manage Item filter value fields treat properties whose
normalizedKeyisIdas literal identifiers. They do not call the finite-listproperty-itemsendpoint for those properties; lookup and status properties continue loading their selectable values normally. Checkbox/boolean properties synthesize localized Yes/No values locally. Existing filters may restore their property key before async property metadata arrives; the field reclassifies that key when metadata resolves and ignores any obsolete remote response, so edit mode retains the boolean Values tab (#1237). ChartDataService.clearCache(dashboardId?)clearsLimitedHttpServicecache entries by chart item id (serviceConfig.dashboardId) when an id is passed, or clears all chart responses when omitted. Scoped clears also bypass matching in-flight requests so the next fetch cannot reuse stale data after a host create/edit/delete action.DashboardViewer.reloadPage()is a host-facing mutation refresh API: it clears the dashboard chart HTTP cache before triggering all chart items, so calls such asviewer()?.reloadPage()always send fresh backend requests.
Development
# Build only the dashboard-builder package
pnpm build:db
# Focused tests for changed specs
pnpm --filter @masterteam/dashboard-builder test -- --watch=false --include=<path>.spec.tsDo not use the full @masterteam/components build for routine component work
unless the caller explicitly approves it.
License
MIT
