Design Philosophy
The Vyuh Dashboard Editor is built on six core principles that guide every architectural decision. Understanding these principles helps you work with the system effectively and extend it in ways that remain consistent with its design.
Principle 1: Command-Driven Mutations
Every layout change in the dashboard is expressed as a reversible command. Commands are small, focused objects that know how to execute a mutation and undo it.
// All mutations go through commands -- never mutate layout directly
await engine.addRow(); // AddRowCommand
await engine.splitRow(rowId, 3); // SplitRowCommand
await engine.addComponent( // AddComponentCommand
rowId: rowId,
columnId: columnId,
componentKey: 'stat_card',
);Why this matters:
- Undo/redo is automatic. The history extension records every command.
- Persistence is reliable. The persistence extension marks dirty on every command.
- Analytics are free. The analytics extension observes command events.
- Testing is straightforward. Commands are pure functions from layout to layout.
The engine never exposes raw state mutation. Instead, executeCommand() wraps the mutation in a MobX runInAction, updates the observable layout, and emits structured events to all extensions.
Principle 2: Component Descriptor Pattern
Components are defined declaratively through ComponentDescriptor instances rather than class hierarchies. This follows the same "Configuration as Code" pattern used by EntityConfiguration in the Vyuh Entity System.
// Declarative -- pass behavior as constructor parameters
final myComponent = ComponentDescriptor(
schemaType: 'app.my_widget',
componentKey: 'my_widget',
displayName: 'My Widget',
properties: () => buildMyProperties(),
render: (context, props) => MyWidgetView(properties: props),
configPanel: (context, props) => MyWidgetConfigPanel(properties: props),
);Why this matters:
- No boilerplate. One constructor call replaces an entire class definition.
- Consistent interface. Every component has the same shape: properties factory, render callback, optional config panel and thumbnail.
- Composable. Components are just data. You can generate them, filter them, transform them.
The abstract DashboardComponentConfiguration base class still exists for advanced cases where you need inheritance, but ComponentDescriptor is the recommended approach.
Principle 3: Extension Architecture
The engine is deliberately minimal. All cross-cutting concerns -- history, persistence, selection, analytics -- are implemented as extensions that observe engine events.
// Extensions are pluggable and independent
engine.addExtension(DashboardHistoryExtension());
engine.addExtension(DashboardPersistenceExtension(registry: registry));
engine.addExtension(DashboardSelectionExtension());
engine.addExtension(DashboardAnalyticsExtension());Each extension implements three lifecycle methods:
| Method | Purpose |
|---|---|
attach(engine) | Receive a reference to the engine API |
detach() | Clean up when removed |
onEvent(event) | React to engine events |
Why this matters:
- Open for extension, closed for modification. New behaviors do not require engine changes.
- Testing in isolation. Each extension can be tested independently with a mock engine.
- Optional features. An app that needs custom telemetry, persistence, or refresh behavior can add or replace extensions around the same engine events.
Principle 4: Flex-Based Grid
The layout uses a flex-based grid with 6 flex units per row. Columns within a row specify their flex values, and the system distributes width proportionally.
Constants:
| Constant | Value | Purpose |
|---|---|---|
totalFlexPerRow | 6 | Total flex units per row |
minColumnFlex | 1 | Minimum flex for any column |
maxColumns | 6 | Maximum columns per row |
maxRows | 25 | Maximum rows per dashboard |
Why this matters:
- Simple mental model. Six units is easy to reason about: halves (3+3), thirds (2+2+2), quarters with emphasis (3+1+1+1).
- Automatic normalization. When columns are added or removed, flex values are redistributed to always sum to 6.
- Responsive by nature. Flex-based sizing adapts to any screen width without breakpoint logic.
Principle 5: PropertyCollection Configuration
Component configuration uses the Property System's PropertyCollection rather than ad-hoc JSON maps. This gives you type-safe, validated, observable properties with built-in editor UI.
properties: () => (PropertyCollectionBuilder()
..group('content', 'Content')
..string('title', 'Title', defaultValue: 'Metric')
..integer('value', 'Value', defaultValue: 0)
..enumeration('color', 'Color',
values: ['blue', 'green', 'red'],
defaultValue: 'blue',
)
).buildCollection(),Why this matters:
- Type safety. Properties are validated at creation time with validators.
- Built-in editor. The
TabbedPropertyCollectionEditorrenders a config panel automatically. - Serialization.
PropertyCollection.toJson()andfromJson()handle persistence. - Reactivity. Properties are MobX observables. Changes propagate automatically.
Principle 6: Event-Driven Reactivity
The engine emits structured events for every state change. Extensions and UI components react to these events rather than polling or watching raw state.
Event types:
| Event | Emitted When |
|---|---|
DashboardLayoutChanged | Layout observable is updated (any reason) |
DashboardCommandExecuted | A command is executed for the first time |
DashboardCommandUndone | A command is undone |
DashboardCommandRedone | A command is redone |
DashboardBatchStarted | A batch operation begins |
DashboardBatchEnded | A batch operation completes |
The engine wraps all mutations in MobX runInAction, ensuring that UI rebuilds are batched and efficient. The Observable layout is the single source of truth, and Observer widgets in the UI tree rebuild only when the layout actually changes.
Putting It Together
These six principles create a system where:
- The engine is a thin state kernel with no opinions about UI or persistence.
- Commands provide a testable, reversible mutation layer.
- Extensions add cross-cutting concerns without coupling.
- The flex grid gives a simple, predictable layout model.
- PropertyCollection ensures type-safe, reactive configuration.
- Events connect everything through a loosely coupled communication channel.
This architecture allows the dashboard editor to serve a wide variety of use cases -- from LMS analytics dashboards to manufacturing quality control panels -- without requiring modifications to the core system.
Next Steps
- Architecture -- Deep dive into the engine internals
- Layout Model -- Understand the row/column/component hierarchy
- Component System -- Master the ComponentDescriptor pattern