UI Component Migration
Migrate legacy Magento UI Component XML definitions to Nebula's JSON-driven system using the automated transformer tool.
Prerequisites
The Transformer Tool
Command Syntax
bin/magento nebula:ui:transform <path> [--output-dir=<dir>]path/to/my_listing.xmlpath/to/ui_component/app/code/Vendor/ModuleOutput Structure
Output is written to var/nebula-ui-transformer/<source-name>/ by default. It is a draft — review, edit, then copy.
var/nebula-ui-transformer/Module/
├── grid/
│ └── my_listing.json # Nebula grid definition
├── form/
│ └── my_form.json # Nebula form definition
├── layout/
│ └── vendor_module_index.xml # Layout XML stub
└── README.md # Summary + warningsStep-by-Step Workflow
Run the transformer
Point the command at your module, directory, or a single XML file.
# Transform an entire module
bin/magento nebula:ui:transform app/code/Vendor/Module
# Transform a single file
bin/magento nebula:ui:transform app/code/Vendor/Module/view/adminhtml/ui_component/my_listing.xml
# Custom output directory
bin/magento nebula:ui:transform app/code/Vendor/Module --output-dir=var/my-migrationReview the generated JSON
Open the generated JSON and check the _meta.warnings array. Every warning needs attention before going live.
dataSource.provider (will say TODO_NEBULA_PROVIDER_CLASS), column types, filter mappings, and action definitions.Create a Nebula DataProvider
Replace TODO_NEBULA_PROVIDER_CLASS with a real provider. For simple grids, use CollectionProvider. For complex cases, implement the interface.
<?php
declare(strict_types=1);
namespace Vendor\Module\Model\Grid;
use Qoliber\NebulaGrid\Api\DataProviderInterface;
class MyDataProvider implements DataProviderInterface
{
public function __construct(
private readonly \Vendor\Module\Model\ResourceModel\Entity\CollectionFactory $collectionFactory
) {
}
public function getData(array $params): array
{
$collection = $this->collectionFactory->create();
// Apply filters, sorting, pagination from $params
return [
'items' => $collection->toArray()['items'],
'totalRecords' => $collection->getSize(),
];
}
}Copy JSON to your module
Move the generated JSON files to the correct Nebula directories.
# Grids go to nebula/grid/
cp var/nebula-ui-transformer/Module/grid/my_listing.json \
app/code/Vendor/Module/view/adminhtml/nebula/grid/my_listing.json
# Forms go to nebula/form/
cp var/nebula-ui-transformer/Module/form/my_form.json \
app/code/Vendor/Module/view/adminhtml/nebula/form/my_form.jsonCopy layout XML
Copy the generated layout stub to your module's layout directory.
cp var/nebula-ui-transformer/Module/layout/vendor_module_index.xml \
app/code/Vendor/Module/view/adminhtml/layout/vendor_module_index.xmlRemove the legacy uiComponent reference
The generated layout XML replaces the legacy <uiComponent> tag with a Nebula block.
<referenceContainer name="content">
<uiComponent name="my_listing"/>
</referenceContainer><referenceContainer name="content">
<block class="Qoliber\NebulaGrid\Block\Grid"
name="nebula.my_listing"
after="-">
<arguments>
<argument name="grid_id"
xsi:type="string">my_listing
</argument>
</arguments>
</block>
</referenceContainer>Test the page
php bin/magento cache:flushNavigate to the admin page and verify: data loads, columns render, filters work, mass actions function, form fields display and save.
Fine-tune
After the basic migration works, enhance with Nebula features:
Grid Migration
Column Type Mapping
| Legacy (XML) | Nebula (JSON) |
|---|---|
| text (default) | text |
| date / date component | date |
| <selectionsColumn> | Skipped (auto-handled) |
| <actionsColumn> | actions |
Filter Type Mapping
| Legacy (XML) | Nebula (JSON) |
|---|---|
| text | text |
| textRange | range |
| dateRange | date |
| select | select |
Mass Actions
URL-based mass actions are extracted automatically. Callback-based actions generate a warning and must be converted to URL-based controller actions.
"massActions": [
{ "label": "Delete", "url": "vendor_module/entity/massDelete" },
{ "label": "Disable", "url": "vendor_module/entity/massDisable" }
]Form Migration
Field Type Mapping
| Legacy (XML) | Nebula (JSON) |
|---|---|
| input | text (or number) |
| select | select |
| multiselect | multiselect |
| checkbox (prefer=toggle) | toggle |
| checkbox | checkbox |
| textarea | textarea |
| wysiwyg | wysiwyg |
| date | date |
| visible=false | hidden |
Validation Rule Mapping
| Legacy (XML) | Nebula (JSON) |
|---|---|
| required-entry | required: true |
| validate-email | email: true |
| min_text_length | minLength: <n> |
| max_text_length | maxLength: <n> |
| min / max | min / max: <n> |
| Other rules | Warning (manual review) |
Form Layout Structure
Legacy <fieldset> elements become Nebula sections within a row/column layout. The transformer generates this structure automatically.
"layout": [
{
"id": "general_row",
"type": "row",
"children": [{
"id": "general_column",
"type": "column",
"children": [{
"id": "general_section",
"type": "section",
"label": "General",
"open": true,
"fields": ["title", "is_active"]
}]
}]
}
]EavForm block class and expect significant manual work for dynamic attribute groups.Common Warnings & Fixes
TODO_NEBULA_PROVIDER_CLASSMeaning: Every generated JSON has this placeholder. The transformer cannot auto-create data providers.
Fix: Create a Nebula data provider class and replace the placeholder with the fully qualified class name.
Actions column requires manual action mappingMeaning: The legacy actionsColumn was detected but action URLs are defined in PHP, not XML.
Fix: Add action definitions (edit, delete) with routes and params to the JSON actions column.
Potential EAV form detectedMeaning: The form matches a known EAV entity (product, category).
Fix: Use EavForm block class in layout XML. Manual configuration needed for dynamic attribute groups.
Mass action uses callbackMeaning: The mass action has no URL path — it uses a JavaScript callback.
Fix: Create a controller action that accepts selected IDs via POST, then add the URL to the JSON.
Validation rule requires manual reviewMeaning: The rule is not in the transformer's supported set.
Fix: Check if Nebula supports it natively in nebula-validate.js, or implement custom validation.
No layout XML reference foundMeaning: The component may be referenced from a different module or theme.
Fix: Create the layout XML manually using the generated stub as a template.
Before & After Examples
Grid Example: CMS Pages
A typical listing with ID, title, status columns, mass delete, and row actions.
<listing ...>
<dataSource name="cms_page_listing_data_source">
<argument name="dataProvider" xsi:type="configurableObject">
<argument name="class" xsi:type="string">
Magento\Cms\Ui\Component\DataProvider
</argument>
</argument>
<settings>
<requestFieldName>page_id</requestFieldName>
<primaryFieldName>page_id</primaryFieldName>
</settings>
<aclResource>Magento_Cms::page</aclResource>
</dataSource>
<columns name="cms_page_columns">
<column name="page_id">
<settings>
<filter>textRange</filter>
<label>ID</label>
<sorting>asc</sorting>
</settings>
</column>
<column name="title">
<settings>
<filter>text</filter>
<label>Title</label>
</settings>
</column>
<column name="is_active" component="Magento_Ui/js/grid/columns/select">
<settings>
<filter>select</filter>
<label>Status</label>
<options class="Magento\Cms\Model\Page\Source\IsActive"/>
</settings>
</column>
<actionsColumn name="actions" class="Magento\Cms\Ui\Component\Listing\Column\PageActions"/>
</columns>
</listing>{
"id": "cms_page_listing",
"acl": "Magento_Cms::page",
"dataSource": {
"provider": "Qoliber\\NebulaGrid\\Model\\DataProvider\\CollectionProvider",
"config": {
"collection": "Magento\\Cms\\Model\\ResourceModel\\Page\\Collection",
"requestFieldName": "page_id",
"primaryFieldName": "page_id"
}
},
"settings": {
"pageSize": 20,
"defaultSort": { "field": "page_id", "direction": "asc" }
},
"columns": {
"page_id": {
"label": "ID", "type": "text",
"position": 10, "sortable": true, "filter": "range"
},
"title": {
"label": "Title", "type": "text",
"position": 20, "sortable": true,
"filter": "text", "searchable": true
},
"is_active": {
"label": "Status", "type": "text",
"position": 50, "filter": "select",
"@use": "snippet/status_badge",
"options": {
"1": { "label": "Enabled", "class": "success" },
"0": { "label": "Disabled", "class": "neutral" }
}
},
"actions": {
"type": "actions", "position": 60,
"actions": {
"edit": {
"label": "Edit",
"route": "cms/page/edit",
"params": { "page_id": "{{page_id}}" }
}
}
}
},
"massActions": [
{ "label": "Delete", "url": "cms/page/massDelete" }
]
}Form Example: CMS Page Edit
A form with two fieldsets (General and Content), toggle field, and WYSIWYG editor.
<form ...>
<dataSource name="cms_page_form_data_source">
<settings>
<requestFieldName>page_id</requestFieldName>
<primaryFieldName>page_id</primaryFieldName>
<submitUrl path="cms/page/save"/>
</settings>
</dataSource>
<fieldset name="general" sortOrder="10">
<settings><label>General</label></settings>
<field name="title" formElement="input" sortOrder="10">
<settings>
<label>Page Title</label>
<validation>
<rule name="required-entry" xsi:type="boolean">true</rule>
</validation>
</settings>
</field>
<field name="is_active" formElement="checkbox" sortOrder="20">
<settings><label>Enable Page</label><dataType>boolean</dataType></settings>
<formElements>
<checkbox><settings><prefer>toggle</prefer></settings></checkbox>
</formElements>
</field>
</fieldset>
<fieldset name="content" sortOrder="20">
<settings><label>Content</label></settings>
<field name="content" formElement="wysiwyg" sortOrder="10">
<settings><label>Content</label></settings>
</field>
</fieldset>
</form>{
"id": "cms_page_form",
"dataSource": {
"provider": "Vendor\\Module\\Model\\Form\\CmsPageDataProvider",
"config": {
"identifierParam": "page_id",
"primaryFieldName": "page_id"
}
},
"settings": {
"title": "CMS Page",
"identifierField": "page_id",
"backUrl": "cms/page/index",
"saveUrl": "cms/page/save"
},
"layout": [
{
"id": "general_row", "type": "row",
"children": [{ "id": "general_column", "type": "column",
"children": [{ "id": "general_section", "type": "section",
"label": "General", "open": true,
"fields": ["title", "is_active"] }] }]
},
{
"id": "content_row", "type": "row",
"children": [{ "id": "content_column", "type": "column",
"children": [{ "id": "content_section", "type": "section",
"label": "Content", "open": true,
"fields": ["content"] }] }]
}
],
"fieldsets": {
"general": {
"label": "General", "position": 10, "open": true,
"fields": {
"title": {
"label": "Page Title", "type": "text",
"position": 10, "required": true,
"validation": { "required": true }
},
"is_active": {
"label": "Enable Page", "type": "toggle",
"position": 20
}
}
},
"content": {
"label": "Content", "position": 20, "open": true,
"fields": {
"content": {
"label": "Content", "type": "wysiwyg",
"position": 10
}
}
}
}
}Tips
- Start with simple grids. Listing pages with flat tables are the easiest to migrate.
- Use _meta for debugging. Remove the _meta block before committing to production.
- Leverage deep-merge. If Nebula already ships a grid for a core entity, you only need to overlay your customizations.
- Keep legacy XML. NebulaUiRemoval suppresses them at runtime, so they are harmless to leave in place until migration is verified.