Cheat sheet¶
One page of the things you look up most. Every item links to the page that explains it.
Where files go¶
| What | Path in your module |
|---|---|
| Grid definition | view/adminhtml/grid/<id>.json |
| Form definition | view/adminhtml/form/<id>.json ("type": "eav" for EAV forms) |
| Snippet | view/adminhtml/snippet/<id>.json plus a .phtml under view/adminhtml/templates/ |
| Skin | view/adminhtml/nebula/skin/<id>.json |
| Layout handle that mounts a grid | view/adminhtml/layout/<route>_<controller>_<action>.xml |
| Aliases and renderers | etc/di.xml |
| Module stylesheet and script | view/adminhtml/web/css/*.css, view/adminhtml/web/js/*.js, loaded from layout <head> |
The same id in two modules is an overlay, merged in module load order. The definition engine
Minimal grid¶
{
"id": "vendor_thing_listing",
"acl": "Vendor_Module::thing",
"dataSource": { "provider": "grid.collection", "config": { "collection": "vendor_thing.collection" } },
"settings": { "pageSize": 20, "defaultSort": { "field": "thing_id", "direction": "asc" } },
"columns": {
"thing_id": { "label": "ID", "type": "text", "sortable": true, "filter": "range", "position": 10 },
"name": { "label": "Name", "type": "text", "sortable": true, "filter": "text", "searchable": true, "position": 20 }
}
}
<referenceContainer name="content">
<block class="Nebula\Grid\Block\Grid" name="nebula.vendor_thing_listing">
<arguments><argument name="grid_id" xsi:type="string">vendor_thing_listing</argument></arguments>
</block>
</referenceContainer>
Your first grid · Grid reference · Grid examples
Minimal form¶
{
"id": "vendor_thing_edit",
"acl": "Vendor_Module::thing",
"route": "vendor_module_thing_edit",
"dataSource": { "provider": "vendor_thing.form", "config": { "identifierParam": "thing_id" } },
"settings": { "title": "Thing", "identifierField": "thing_id", "backUrl": "vendor_module/thing/index" },
"fieldsets": {
"general": {
"label": "General", "position": 10, "open": true,
"fields": {
"thing_id": { "type": "hidden", "position": 10 },
"name": { "label": "Name", "type": "text", "required": true, "position": 20 }
}
}
}
}
saveUrl defaults to */*/save. Your first form · Form reference
Overlay recipes¶
| Do this | Write this |
|---|---|
| Add a column | { "columns": { "my_col": { "label": "Mine", "type": "text", "position": 55 } } } |
| Relabel a field | { "fieldsets": { "general": { "fields": { "title": { "label": "Name" } } } } } |
| Remove a column, field or fieldset | { "columns": { "old": { "$remove": true } } } |
| Remove one item of an id-keyed list | { "settings": { "massActions": [ { "id": "delete", "$remove": true } ] } } |
| Change a field's type | { "fieldsets": { "content": { "fields": { "content": { "type": "textarea" } } } } } |
| Add a snippet panel | { "fieldsets": { "mine": { "label": "Mine", "position": 25, "renderer": "snippet.my_panel" } } } |
Never write an empty object; "columns": {} deletes the map. Sequence your module after the one you overlay. Grid overlays · Form overlays
DI bindings¶
<type name="Nebula\Grid\Model\Registry\CollectionRegistry">
<arguments><argument name="bindings" xsi:type="array">
<item name="vendor_thing.collection" xsi:type="string">Vendor\Module\Model\ResourceModel\Thing\Collection</item>
</argument></arguments>
</type>
<type name="Nebula\Component\Model\Registry\DataProviderRegistry">
<arguments><argument name="bindings" xsi:type="array">
<item name="vendor_thing.form" xsi:type="string">Vendor\Module\Model\DataProvider\ThingFormDataProvider</item>
</argument></arguments>
</type>
<type name="Nebula\Component\Model\Registry\OptionSourceRegistry">
<arguments><argument name="bindings" xsi:type="array">
<item name="vendor.thing.status" xsi:type="string">Vendor\Module\Model\Source\Status</item>
</argument></arguments>
</type>
<type name="Nebula\Component\Model\SnippetViewModelRegistry">
<arguments><argument name="viewModels" xsi:type="array">
<item name="my_panel" xsi:type="string">Vendor\Module\ViewModel\MyPanel</item>
</argument></arguments>
</type>
Definitions name aliases, never classes, and every block above belongs in etc/di.xml: an argument declared in etc/adminhtml/di.xml replaces the global one for that area and drops every other module's bindings — nebula:doctor reports it. Extension points
Admin routes¶
Nine admin front names ship. Nebula_Core declares the nebula front name and four more modules join it, each declaring <module name="..." before="Nebula_Core"/> in its own etc/adminhtml/routes.xml, so nebula/grid/*, nebula/form/*, nebula/merchandiser/* and nebula/taxrate/* sit side by side under one front name.
| Front name | Module | Serves |
|---|---|---|
nebula |
Nebula_Core, joined by Nebula_Grid, Nebula_Form, Nebula_Theme and Nebula_Merchandiser |
Grid and form endpoints, menu pinning, the merchandiser, and the entity screens Nebula replaces such as tax rates, ratings and agreements |
nebuladirective |
Nebula_Directive |
The variable and widget pickers the editors open |
nebulaemail |
Nebula_EmailTemplateEditor |
The email template editor, its preview and its test send |
nebula_landing |
Nebula_LandingPage |
Landing page screens |
nebulamedia |
Nebula_Media |
The media library screen and the browser's JSON calls |
nebulamenu |
Nebula_Menu |
Nothing in the shipped admin; pinning goes through nebula/menupin |
nebulasystem |
Nebula_System |
Admin notifications, custom variables, grid export filters |
nebulapasskey |
Nebula_Passkey |
Admin passkey enrolment, sign-in and management |
nebula_theme_switcher |
Nebula_ThemeSwitcher |
Switching the whole admin between Nebula and another registered admin theme |
Two storefront front names ship as well. nebula_landing serves landing pages, and passkey serves the customer passkey screens under the route id nebulapasskey.
JSON endpoints¶
| Endpoint | Verb | Guarded by |
|---|---|---|
nebula/grid/data?id=<grid_id> |
GET | the acl of the resolved grid definition |
nebula/grid/export?grid_id=<grid_id>&format=csv |
GET | the acl of the resolved grid definition |
nebula/selection/search?source=<alias> |
GET | getAclResource() on the registered selection source |
nebula/form/save |
POST | the acl of the resolved form definition |
nebula/form/validate |
POST | the acl of the resolved form definition |
nebula/menupin |
POST | Magento_Backend::admin |
nebula/menupin/reorder |
POST | Magento_Backend::admin |
The grid parameters are the request contract in Grid reference. Selection search is paged and returns 100 rows at most, described under Extension points. Save and validate both take a JSON body. Save answers {"success": true, "entityId": 12}, and validate answers {"valid": true} or {"valid": false, "errors": {...}}. Saving and validation
nebula/menupin toggles one pin from a posted item_id and answers {"success": true, "pinned": true, "item_id": "..."}. nebula/menupin/reorder takes order, a JSON-encoded array of menu item ids in the new order. Both URLs are built with _nosecret, so neither carries the admin secret key segment. Both are still POSTs, so send form_key in the body. Both store against the signed-in administrator's own user record, which is why one administrator's pins never show up for another. Navigation
One pin endpoint, not two
Nebula_Menu also ships nebulamenu/pin/index. It takes the same item_id and writes the same storage, but nothing in the shipped admin calls it. Call nebula/menupin.
Saved grid views¶
Read or seed a saved view through the repository rather than writing to the nebula_grid_view table.
use Nebula\Grid\Api\V1\Data\GridViewInterfaceFactory;
use Nebula\Grid\Api\V1\GridViewRepositoryInterface;
$view = $this->gridViewFactory->create()
->setAdminUserId($adminUserId)
->setGridId('vendor_thing_listing')
->setName('Open items')
->setIsDefault(true)
->setSortOrder(10)
->setConfig((string) json_encode(['filters' => ['status' => 'open']]));
$this->gridViewRepository->save($view);
The repository has four methods: save(), getById(int $viewId), deleteById(int $viewId) and getListForUser(int $adminUserId, string $gridId). A view belongs to one grid and one administrator, which is why the listing call takes both. config is the whole view state as a JSON string. Both interfaces are bound by <preference> in code/Nebula/Grid/etc/di.xml, and neither carries an @api tag yet, so pin your version if you depend on them. Saved views
Commands¶
| Command | When |
|---|---|
bin/magento nebula:lint |
After writing or overlaying a definition; in CI |
bin/magento nebula:compile |
Production mode, after deploy; --verify in CI |
bin/magento nebula:doctor |
Before a support request |
bin/magento nebula:crud:generate <table> --module=Vendor_Module --dry-run |
Scaffold a CRUD module |
bin/magento nebula:ui:transform <path> |
Convert UI Component XML to draft definitions |
bin/magento nebula:passkey:reset-two-step --user=<user> |
Unlock an administrator |
Cache cleans¶
| After changing | Run |
|---|---|
| A definition (default mode) | bin/magento cache:clean nebula_definitions |
| A definition (production mode) | bin/magento nebula:compile |
route, replaces, routeWhen |
the above, plus bin/magento cache:clean layout block_html |
| A skin or the Color Scheme setting | bin/magento cache:clean config |
| Nothing, in developer mode | definitions are read live |
Template variables in a snippet¶
| Grid cell | Form section |
|---|---|
record, nebula_context = grid, view_model, column, key, item, value |
record, nebula_context = form, view_model, entity_data, form_block, section |
Module stylesheet and script¶
.vendor-thing { background: var(--nebula-bg-secondary); color: var(--nebula-text-primary); border: 1px solid var(--nebula-border-default); }
<!-- view/adminhtml/layout/default.xml: your script, after the core bundle -->
<referenceContainer name="before.body.end">
<block class="Magento\Framework\View\Element\Template" name="vendor.module.scripts"
template="Vendor_Module::scripts.phtml" after="nebula.component.scripts"/>
</referenceContainer>
document.addEventListener('alpine:init', () => { Alpine.data('vendorThing', () => ({ open: false })); });
No LESS, no RequireJS, and no Tailwind utilities from a module's own markup — the theme does not scan your templates, so the class is emitted nowhere. Plain CSS over the tokens instead. Translations: JsPhrasesProviderInterface → window.Nebula.t(). Types: web/types/nebula.d.ts. CSS and JavaScript in your module
Config paths you will set¶
| Path | Purpose |
|---|---|
nebula/theme/skin |
Color scheme id |
nebula/theme/menu_position |
sidebar, top or magento (the icon rail) |
nebula_passkey/general/rp_id |
Relying Party ID for passkeys |
twofactorauth/general/force_providers |
google,nebula_passkey by default |
nebula_theme_switcher/general/enabled and active_theme |
The admin theme every administrator gets, set at default scope |
csp/mode/admin/report_only |
0 to enforce the admin CSP |