Files
AeonDumpManager/docs/research/admin-dump-manager-ui.md
T

20 KiB

Research: Administration UI for AeonDumpManager (list/create/delete SQL dumps)

Target: Shopware 6.6 administration plugin development. Differences for 6.7 noted per section.

Summary / Recommendations

  • Menu: Register a module with Shopware.Module.register() and a navigation entry whose parent is an existing settings-area id (e.g. sw-settings or a dedicated top-level is not allowed for plugins — see §1). For a dev-tooling plugin like AeonDumpManager, nest under Settings rather than trying to create a first-level item; Shopware Store review explicitly rejects plugins that add first-level menu entries.
  • Module structure: One module/aeon-dump-manager folder with index.js (routes/navigation/snippets), a single list page component, no detail/create page needed as a separate route — a create action can be a button + modal on the list page since dumps aren't Shopware entities. Bind sw-data-grid directly to a plain JS array (this.dumps) returned from your own API, not an EntityCollection/repository.search() flow.
  • Backend communication: Write one custom PHP controller extending AbstractController, scoped with #[Route(defaults: [PlatformRequest::ATTRIBUTE_ROUTE_SCOPE => [ApiRouteScope::ID]])] (or AdministrationRouteScope::ID) under /api/_action/aeon-dump-manager/..., tagged as a service (shopware.controller context via routes.xml/routes.php auto-discovery). Pair it with a small JS ApiService subclass (Shopware.Classes.ApiService) registered via Application.addServiceProvider, injected into the component via inject: ['aeonDumpManagerApiService']. Do not rely on client-side ACL checks alone (acl.can() only hides UI) — do the real permission check server-side, e.g. via AclRoleDefinition privilege validation, since ACL in the admin can be bypassed by direct API calls.
  • TypeScript: There is no first-class "TypeScript for admin plugins" guide; official support is limited to extending the webpack build (build/webpack.config.js, pre-6.7) or Vite config (vite.config.mts, 6.7+) to add ts-loader/@babel/preset-typescript, and core is now written largely in TS internally. Unless the project already needs TS, skip it — plain JS with the existing Vue 2-options-API-via-compat conventions is the supported default and avoids one more moving build part for a small plugin.
  • File size formatting: Do not reimplement byte formatting — the admin core already ships fileSize(bytes, locale) under Shopware.Utils.format.fileSize. Reuse it directly in the grid column renderer.
  • 6.7: Biggest relevant changes are the Vue 2 compat-layer removal (must be genuinely Vue 3 compatible components — no $set/$delete, watch this.$… internal API usage), Vuex→Pinia (Shopware.StateShopware.Store, only relevant if you add a custom store), and Webpack→Vite (only relevant if you extend the build). None of this blocks writing the module against 6.6 now, provided the components avoid deprecated Vue 2-only patterns.

1. Admin menu registration

A plugin adds an admin module (and its menu entry) via Shopware.Module.register(moduleId, config) called from src/Resources/app/administration/src/module/<name>/index.js, imported by main.js. The navigation property of the config is an array of navigation-entry objects; the documented fields are id, label, path (a dot-notation route id matching a routes entry), icon, color, parent, and position (numeric ordering). Add Menu Entry — developer.shopware.com

Important restriction: plugin modules cannot add entries at the first level of the main menu — a parent pointing at an existing menu node (Shopware supports infinite nesting) is required. The Shopware Store plugin review process explicitly rejects plugins that create their own first-level menu entry. Add Menu Entry — developer.shopware.com

ACL-gated visibility: a navigation entry (and individual routes entries via meta.privilege) can carry a privilege key so the menu item / route is hidden unless the logged-in admin user has that privilege — privileges are declared separately via Shopware.Service('privileges').addPrivilegeMappingEntry(...) in a module's acl/index.js. Adding Permissions — developer.shopware.com

Where to hook in for a dev-tooling plugin: Shopware's own settings modules (e.g. sw-settings) are the conventional parent for administrative/operational tooling that isn't a merchandising concept (products, orders, etc.). Since AeonDumpManager already exposes a store-config panel via config.xml (per CLAUDE.md), placing the module's navigation entry as a child of the existing settings area is the natural, non-first-level location and matches the plugin's job (dev/ops tooling, not a customer-facing catalog feature).

6.7 differences: The Module.register / navigation API itself is not called out as changed in the 6.7 upgrade materials found. The changes that do land in 6.7 and affect any admin module (menu module included) are:

No meteor-admin-sdk vs. core-plugin distinction is relevant here: meteor-admin-sdk (the "app extension SDK", formerly admin-extension-sdk) is the mechanism for Shopware Apps (no PHP backend, communicate over postMessage/iframe), not for a classic PHP+JS plugin like AeonDumpManager, which uses the core plugin administration extension mechanism documented above. This plugin is a PHP plugin (shopware-platform-plugin, confirmed by CLAUDE.md), so the core Shopware.Module.register path is the correct one, not the SDK.

2. Building the admin UI module structure

Conventional directory layout under src/Resources/app/administration/src/:

main.js                              # imports the module: import './module/aeon-dump-manager';
module/aeon-dump-manager/
  index.js                           # Shopware.Module.register(...) — routes, navigation, snippets
  acl/index.js                       # addPrivilegeMappingEntry (optional but recommended)
  snippet/
    en-GB.json
    de-DE.json
  page/
    aeon-dump-manager-list/
      index.js                       # Shopware.Component.register(...)
      aeon-dump-manager-list.html.twig

index.js registers routes (e.g. list: { component: 'aeon-dump-manager-list', path: 'list' }), the navigation array (§1), and snippets per locale. Add Custom Module — developer.shopware.com, Add Custom Route — developer.shopware.com

List rendering — plain array, not EntityCollection: sw-data-grid is documented and used with a plain array of plain objects assigned to component data(), not necessarily an entity repository result:

<sw-data-grid :data-source="dataSource" :columns="columns"></sw-data-grid>
data() {
    return {
        dataSource: [ { id: 'uuid1', company: 'Wordify', name: 'Portia Jobson' }, ... ],
        columns: [ { property: 'name', label: 'Name' }, { property: 'company', label: 'Company' } ]
    };
}

Using the Data Grid Component — developer.shopware.com

This is exactly the right fit for AeonDumpManager: dump files are filesystem entries, not DAL entities, so the list page's data() should hold the array returned by your custom API (§3) — each row { id/filename, createdAt, name, sizeBytes } — with a columns config of name, a datetime column (format via existing Shopware date filter/util), and a size column whose label renders Shopware.Utils.format.fileSize(row.sizeBytes) (§5). Do not build an EntityCollection or wire a repositoryFactory for this list — that machinery is for DAL entities and would be pure overhead here.

Create/delete flow: a "Create dump" sw-button triggers your API service's create call and reloads the list; per-row delete uses a row action (sw-context-button/action column) that opens a confirmation dialog before calling delete. Shopware's own standard confirmation component is sw-confirm-modal (added in 6.3.5.0 as the standard confirm-modal component for exactly this kind of "are you sure you want to delete" flow) — reuse it rather than hand-rolling a sw-modal yes/no dialog. changelog: added-sw-confirm-modal.md — github.com/shopware/shopware, sw-confirm-modal — component-library.shopware.com

3. Frontend-to-backend communication

JS side: define a custom API service class extending Shopware.Classes.ApiService, register it through Shopware.Application.addServiceProvider('aeonDumpManagerApiService', (container) => { const initContainer = Application.getContainer('init'); return new AeonDumpManagerApiService(initContainer.httpClient, container.loginService); }), and inject it into components with inject: ['aeonDumpManagerApiService'] → available as this.aeonDumpManagerApiService. The base class supplies getBasicHeaders() (auth token handling) and ApiService.handleResponse() for uniform error handling; a GET/POST/DELETE call is this.httpClient.get(route, { headers: this.getBasicHeaders() }) etc. Adding Services — developer.shopware.com, Making API Requests — developer.shopware.com

PHP side: create a controller extending AbstractController, scoped to the admin/API domain via the class-level attribute

#[Route(defaults: [PlatformRequest::ATTRIBUTE_ROUTE_SCOPE => [ApiRouteScope::ID]])]
class DumpController extends AbstractController
{
    #[Route(
        path: '/api/_action/aeon-dump-manager/dumps',
        name: 'api.action.aeon_dump_manager.dumps.list',
        methods: ['GET']
    )]
    public function list(Context $context): JsonResponse { ... }
}

This mirrors the core ApiController's own pattern (#[Route(defaults: [PlatformRequest::ATTRIBUTE_ROUTE_SCOPE => [ApiRouteScope::ID]])] at class level, with /api/_action/... action routes at method level, e.g. its clone action at /api/_action/clone/{entity}/{id}). ApiController.php — github.com/shopware/core (trunk); Administration-specific controllers use AdministrationRouteScope::ID from Shopware\Administration\Framework\Routing\AdministrationRouteScope instead when the endpoint is administration-only rather than general Admin API. AdministrationController.php — github.com/shopware/administration (trunk)

Controller discovery uses the plugin's src/Resources/config/routes.php (or routes.xml) importing ../../Controller/*Controller.php via the attribute type loader — the same auto-discovery mechanism documented for storefront controllers, generalized to any controller directory. Add Custom Controller — developer.shopware.com

ACL/authorization: All admin and API routes are authentication-protected by default (bearer token from the admin's own OAuth session, attached automatically by getBasicHeaders()/loginService on the JS side — no separate manual OAuth dance needed in the component code). UPGRADE-6.1.md — github.com/shopware/shopware Client-side privilege checks (this.acl.can('aeon_dump_manager.viewer'), v-if="acl.can(...)") only toggle UI visibility and are documented as bypassable via direct API calls — the actual authorization must be enforced in the controller itself, e.g. by validating against AclRoleDefinition privilege constants the way ApiController::clone() does ($this->validateAclPermissions($context, $definition, AclRoleDefinition::PRIVILEGE_CREATE), throwing ApiException::missingPrivileges() on failure), or via the #[Acl(...)] attribute mechanism that superseded the deprecated @Acl annotation for controller-level privilege gating. Adding Permissions — developer.shopware.com, ApiController.php — github.com/shopware/core (trunk), ADR: controller-configuration-route-defaults — developer.shopware.com

For AeonDumpManager, declare a plugin-specific privilege set (e.g. aeon_dump_manager.viewer / .creator / .deleter) via acl/index.js (addPrivilegeMappingEntry), gate the menu/routes with it client-side for UX, and re-check an equivalent permission (or at minimum require an authenticated admin session, which is already default) server-side before actually running a mysqldump/file-delete operation — these are filesystem/process-level actions, so being defensive here matters more than for a typical read-only entity list.

Gap note: the official docs found do not show a first-party doc page titled specifically "add custom admin API route" with a full worked PHP example (the closest, add-custom-route.html, only covers the Vue Router side, i.e. routes: in Module.register, not the PHP controller) — the PHP-side pattern above is instead verified directly against the shipped ApiController/AdministrationController source in shopware/core and shopware/administration, which is authoritative but means there's no single canonical doc page combining both halves end-to-end for a plugin.

4. TypeScript setup for Shopware 6 administration

There is no dedicated "TypeScript for administration plugins" guide among the fetched official pages. What exists officially:

  • Build extension mechanism (6.6 and earlier): a plugin can provide src/Resources/app/administration/build/webpack.config.js exporting a function that returns webpack config overrides — this is the documented, supported extension point (Extending Webpack). Extending Webpack — developer.shopware.com That page itself contains no TypeScript-specific guidance (no .ts/.tsx, ts-loader, or tsconfig mention) — adding ts-loader or @babel/preset-typescript there is a community pattern layered on top of a documented extension point, not something the official page walks through.
  • 6.7: Webpack → Vite. Per the roadmap doc, plugins are only affected if they currently ship a custom webpack.config.js; those must migrate to a vite.config.mts file (note the .mts extension — TypeScript by convention for the config file itself) under .../administration/src/, and delete the old webpack config/dependencies. Future Development Roadmap: Changing from Webpack to Vite — developer.shopware.com
  • Shopware's own administration source has increasingly moved to .ts/.vue+TS internally (e.g. core factory files like async-component.factory.ts in shopware/shopware trunk), i.e. the core team's own code is TypeScript, but this is not the same as an officially documented, supported path for plugin authors to write their own module in TypeScript out of the box.

Recommendation for AeonDumpManager: skip TypeScript. Per the ponytail ladder, there's no existing TS need in this repo (PHP ^8.1 project per CLAUDE.md, no existing frontend TS tooling), and the admin plugin surface here is small (one list page, one service). Adding a custom webpack.config.js/vite.config.mts + ts-loader/tsconfig just to type a handful of small Vue components is speculative infrastructure for a plugin whose entire admin footprint is one grid and one API service; plain JS following the documented module/service conventions is both fully supported and the shorter path. Revisit only if the plugin's admin surface grows substantially or the team adopts TS project-wide.

5. File size formatting

Shopware's administration core already ships a byte-formatting helper — do not reimplement it. It's documented in the Utils reference (Shopware.Utils.format.fileSize, described as "Formats a number of bytes to a string with a unit") Utils Reference — developer.shopware.com, and its exact implementation in the shopware/shopware monorepo (Administration bundle) is:

// src/Administration/Resources/app/administration/src/core/service/utils/format.utils.ts (shopware/shopware, trunk)
export function fileSize(bytes: number, locale = 'de-DE'): string {
    const denominator = 1024;
    const units = ['B', 'KB', 'MB', 'GB'];

    let result = Number.parseInt(String(bytes), 10);
    let i = 0;

    for (; i < units.length; i += 1) {
        const currentResult = result / denominator;
        if (currentResult < 0.9) break;
        result = currentResult;
    }

    return `${result.toFixed(2).toLocaleString(locale)}${units[i]}`;
}

format.utils.ts — github.com/shopware/shopware (trunk)

Usage in a plugin component: Shopware.Utils.format.fileSize(row.sizeBytes) (accessed via the global Shopware.Utils object per the "Using utility functions" guide). Using utility functions — developer.shopware.com This directly satisfies the "MB/GB human-readable size column" requirement with zero custom code — pass the dump file's byte size straight through it in the sw-data-grid column renderer/slot.

Note it only goes up to GB (no TB step) and defaults to de-DE locale formatting unless a locale is passed — pass the admin's current locale (Shopware.State.get('session').currentLocale in 6.6, or its Shopware.Store equivalent post-Pinia-migration in 6.7) if locale-correct number formatting matters, otherwise the default is fine for dump files (unlikely to exceed a few GB).