NebulaNebula
Migration Guide

UI Component Migration

Migrate legacy Magento UI Component XML definitions to Nebula's JSON-driven system using the automated transformer tool.

Prerequisites

Nebula installed & enabled
All core modules active (NebulaComponent, NebulaGrid, NebulaForm)
PHP 8.1+
Required for the transformer module
JSON definitions understood
Grids in nebula/grid/, forms in nebula/form/
Access to ui_component XML files
The source files the transformer reads

The Transformer Tool

Command Syntax

terminal
bin/magento nebula:ui:transform <path> [--output-dir=<dir>]
Single file
path/to/my_listing.xml
Transforms one component
Directory
path/to/ui_component/
Transforms all XML files in directory
Module path
app/code/Vendor/Module
Finds and transforms all ui_component XML

Output Structure

Output is written to var/nebula-ui-transformer/<source-name>/ by default. It is a draft — review, edit, then copy.

Output directory
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 + warnings

Step-by-Step Workflow

1

Run the transformer

Point the command at your module, directory, or a single XML file.

terminal
# 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-migration
2

Review the generated JSON

Open the generated JSON and check the _meta.warnings array. Every warning needs attention before going live.

Key fields to check: dataSource.provider (will say TODO_NEBULA_PROVIDER_CLASS), column types, filter mappings, and action definitions.
3

Create a Nebula DataProvider

Replace TODO_NEBULA_PROVIDER_CLASS with a real provider. For simple grids, use CollectionProvider. For complex cases, implement the interface.

Grid DataProvider example
<?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(),
        ];
    }
}
4

Copy JSON to your module

Move the generated JSON files to the correct Nebula directories.

terminal
# 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.json
5

Copy layout XML

Copy the generated layout stub to your module's layout directory.

terminal
cp var/nebula-ui-transformer/Module/layout/vendor_module_index.xml \
   app/code/Vendor/Module/view/adminhtml/layout/vendor_module_index.xml
6

Remove the legacy uiComponent reference

The generated layout XML replaces the legacy <uiComponent> tag with a Nebula block.

Before (legacy)
<referenceContainer name="content">
    <uiComponent name="my_listing"/>
</referenceContainer>
After (Nebula)
<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>
If NebulaUiRemoval is active, it already suppresses legacy UI Components globally. You may only need to add the Nebula block.
7

Test the page

terminal
php bin/magento cache:flush

Navigate to the admin page and verify: data loads, columns render, filters work, mass actions function, form fields display and save.

8

Fine-tune

After the basic migration works, enhance with Nebula features:

Badge rendering
Use @use: "snippet/status_badge" on status columns
Action URLs
Configure edit/delete routes in the actions column
Custom snippets
Add specialized rendering (images, product selectors)
Filters & search
Set filter types and searchable flags on columns

Grid Migration

Column Type Mapping

Legacy (XML)Nebula (JSON)
text (default)text
date / date componentdate
<selectionsColumn>Skipped (auto-handled)
<actionsColumn>actions

Filter Type Mapping

Legacy (XML)Nebula (JSON)
texttext
textRangerange
dateRangedate
selectselect

Mass Actions

URL-based mass actions are extracted automatically. Callback-based actions generate a warning and must be converted to URL-based controller actions.

Nebula mass action format
"massActions": [
    { "label": "Delete", "url": "vendor_module/entity/massDelete" },
    { "label": "Disable", "url": "vendor_module/entity/massDisable" }
]

Form Migration

Field Type Mapping

Legacy (XML)Nebula (JSON)
inputtext (or number)
selectselect
multiselectmultiselect
checkbox (prefer=toggle)toggle
checkboxcheckbox
textareatextarea
wysiwygwysiwyg
datedate
visible=falsehidden

Validation Rule Mapping

Legacy (XML)Nebula (JSON)
required-entryrequired: true
validate-emailemail: true
min_text_lengthminLength: <n>
max_text_lengthmaxLength: <n>
min / maxmin / max: <n>
Other rulesWarning (manual review)

Form Layout Structure

Legacy <fieldset> elements become Nebula sections within a row/column layout. The transformer generates this structure automatically.

Generated layout structure
"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"]
            }]
        }]
    }
]
EAV forms (product, category) are detected automatically. The transformer emits a simple-form draft with a warning. Use EavForm block class and expect significant manual work for dynamic attribute groups.

Common Warnings & Fixes

TODO_NEBULA_PROVIDER_CLASS

Meaning: 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 mapping

Meaning: 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 detected

Meaning: 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 callback

Meaning: 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 review

Meaning: 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 found

Meaning: 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.

Legacy XML (simplified)
cms_page_listing.xml
<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>
Nebula JSON
cms_page_listing.json
{
    "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.

Legacy XML (simplified)
cms_page_form.xml
<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>
Nebula JSON
cms_page_form.json
{
    "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.