feat(dump): add settings-page admin UI module

This commit is contained in:
2026-09-14 22:00:26 +02:00
parent 1d8d2de118
commit 92f8605dd0
8 changed files with 344 additions and 2 deletions
+8 -2
View File
@@ -1,2 +1,8 @@
// Import admin module import DumpApiService from './service/dump-api.service';
import './module/swag-example'; import './module/aeon-dump-manager';
Shopware.Application.addServiceProvider('aeonDumpManagerApiService', (container) => {
const initContainer = Shopware.Application.getContainer('init');
return new DumpApiService(initContainer.httpClient, container.loginService);
});
@@ -0,0 +1,19 @@
Shopware.Service('privileges').addPrivilegeMappingEntry({
category: 'permissions',
parent: null,
key: 'aeon_dump_manager',
roles: {
viewer: {
privileges: ['aeon_dump_manager.viewer'],
dependencies: [],
},
creator: {
privileges: ['aeon_dump_manager.creator'],
dependencies: ['aeon_dump_manager.viewer'],
},
deleter: {
privileges: ['aeon_dump_manager.deleter'],
dependencies: ['aeon_dump_manager.viewer'],
},
},
});
@@ -0,0 +1,39 @@
import './acl';
import './page/aeon-dump-manager-list';
import deDE from './snippet/de-DE.json';
import enGB from './snippet/en-GB.json';
Shopware.Module.register('aeon-dump-manager', {
type: 'plugin',
name: 'DumpManager',
title: 'aeon-dump-manager.general.mainMenuItemGeneral',
description: 'aeon-dump-manager.general.descriptionTextModule',
color: '#ff3d58',
icon: 'default-object-storage',
snippets: {
'de-DE': deDE,
'en-GB': enGB,
},
routes: {
list: {
component: 'aeon-dump-manager-list',
path: 'list',
meta: {
parentPath: 'sw.settings.index.system',
privilege: 'aeon_dump_manager.viewer',
},
},
},
// Shown as a card on the Settings page's "System" group, next to
// Plugins/Cache — matches the locked ACL/menu-naming decision. Plugins
// cannot register a first-level main-menu entry.
settingsItem: {
group: 'system',
to: 'aeon.dump.manager.list',
icon: 'default-object-storage',
privilege: 'aeon_dump_manager.viewer',
},
});
@@ -0,0 +1,68 @@
{% block aeon_dump_manager_list %}
<sw-page class="aeon-dump-manager-list">
<template #smart-bar-actions>
<sw-button
v-if="acl.can('aeon_dump_manager.creator')"
variant="primary"
:disabled="pendingJob !== null"
@click="onCreate"
>
{{ $tc('aeon-dump-manager.list.buttonCreate') }}
</sw-button>
</template>
<template #content>
<sw-card-view>
<sw-card>
<sw-data-grid
:data-source="dumps"
:columns="columns"
:is-loading="isLoading"
:show-selection="false"
identifier="filename"
>
<template #before-item-list>
<tr v-if="pendingJob !== null" class="aeon-dump-manager-list__pending-row">
<td colspan="3">
<span>{{ $tc('aeon-dump-manager.list.creating') }}</span>
<sw-progress-bar :max-value="100" :value="pendingJob.percent || 0" />
</td>
</tr>
</template>
<template #column-datetime="{ item }">
{{ formatDatetime(item.datetime) }}
</template>
<template #column-sizeBytes="{ item }">
{{ formatSize(item.sizeBytes) }}
</template>
<template #actions="{ item }">
<sw-context-menu-item
v-if="acl.can('aeon_dump_manager.deleter')"
variant="danger"
@click="onRequestDelete(item.filename)"
>
{{ $tc('aeon-dump-manager.list.buttonDelete') }}
</sw-context-menu-item>
</template>
<template #empty-state>
{{ $tc('aeon-dump-manager.list.empty') }}
</template>
</sw-data-grid>
</sw-card>
</sw-card-view>
</template>
<sw-confirm-modal
v-if="deleteFilename !== null"
:title="$tc('aeon-dump-manager.list.confirmDeleteTitle')"
:text="$tc('aeon-dump-manager.list.confirmDeleteText', 0, { filename: deleteFilename })"
@confirm="onConfirmDelete"
@cancel="onCancelDelete"
@close="onCancelDelete"
/>
</sw-page>
{% endblock %}
@@ -0,0 +1,137 @@
import template from './aeon-dump-manager-list.html.twig';
const { Component } = Shopware;
Component.register('aeon-dump-manager-list', {
template,
inject: ['aeonDumpManagerApiService', 'acl'],
data() {
return {
dumps: [],
isLoading: false,
pendingJob: null,
deleteFilename: null,
pollTimer: null,
};
},
computed: {
columns() {
return [
{
property: 'datetime',
label: this.$tc('aeon-dump-manager.list.columnDatetime'),
allowResize: true,
},
{
property: 'filename',
label: this.$tc('aeon-dump-manager.list.columnFilename'),
allowResize: true,
},
{
property: 'sizeBytes',
label: this.$tc('aeon-dump-manager.list.columnSize'),
align: 'right',
allowResize: true,
},
];
},
},
created() {
this.loadList();
},
beforeDestroy() {
this.stopPolling();
},
methods: {
loadList() {
this.isLoading = true;
return this.aeonDumpManagerApiService.getList()
.then((response) => {
this.dumps = response.dumps;
})
.finally(() => {
this.isLoading = false;
});
},
formatDatetime(value) {
return new Date(value).toLocaleString();
},
formatSize(bytes) {
return Shopware.Utils.format.fileSize(bytes);
},
onCreate() {
this.pendingJob = { percent: 0, status: 'pending' };
this.aeonDumpManagerApiService.create()
.then((response) => {
this.pollJobStatus(response.jobId);
})
.catch((error) => {
this.pendingJob = null;
this.createNotificationError({
message: this.$tc('aeon-dump-manager.list.createFailed', 0, { error: error.message }),
});
});
},
pollJobStatus(jobId) {
this.stopPolling();
this.pollTimer = window.setInterval(() => {
this.aeonDumpManagerApiService.getJobStatus(jobId).then((job) => {
if (job.status === 'done') {
this.stopPolling();
this.pendingJob = null;
this.createNotificationSuccess({ message: this.$tc('aeon-dump-manager.list.createSuccess') });
this.loadList();
return;
}
if (job.status === 'failed') {
this.stopPolling();
this.pendingJob = null;
this.createNotificationError({
message: this.$tc('aeon-dump-manager.list.createFailed', 0, { error: job.errorMessage }),
});
return;
}
this.pendingJob = job;
});
}, 2000);
},
stopPolling() {
if (this.pollTimer !== null) {
window.clearInterval(this.pollTimer);
this.pollTimer = null;
}
},
onRequestDelete(filename) {
this.deleteFilename = filename;
},
onCancelDelete() {
this.deleteFilename = null;
},
onConfirmDelete() {
const filename = this.deleteFilename;
this.deleteFilename = null;
return this.aeonDumpManagerApiService.remove(filename)
.then(() => this.loadList());
},
},
});
@@ -0,0 +1,21 @@
{
"aeon-dump-manager": {
"general": {
"mainMenuItemGeneral": "Dump-Verwaltung",
"descriptionTextModule": "SQL-Datenbank-Dumps erstellen, auflisten und löschen"
},
"list": {
"columnFilename": "Name",
"columnDatetime": "Erstellt am",
"columnSize": "Größe",
"buttonCreate": "Dump erstellen",
"buttonDelete": "Löschen",
"confirmDeleteTitle": "Dump löschen?",
"confirmDeleteText": "Möchten Sie \"{filename}\" wirklich löschen? Dies kann nicht rückgängig gemacht werden.",
"empty": "Keine Dumps gefunden.",
"creating": "Dump wird erstellt…",
"createFailed": "Dump konnte nicht erstellt werden: {error}",
"createSuccess": "Dump erstellt."
}
}
}
@@ -0,0 +1,21 @@
{
"aeon-dump-manager": {
"general": {
"mainMenuItemGeneral": "Dump manager",
"descriptionTextModule": "Create, list and delete SQL database dumps"
},
"list": {
"columnFilename": "Name",
"columnDatetime": "Created at",
"columnSize": "Size",
"buttonCreate": "Create dump",
"buttonDelete": "Delete",
"confirmDeleteTitle": "Delete dump?",
"confirmDeleteText": "Are you sure you want to delete \"{filename}\"? This cannot be undone.",
"empty": "No dumps found.",
"creating": "Creating dump…",
"createFailed": "Could not create dump: {error}",
"createSuccess": "Dump created."
}
}
}
@@ -0,0 +1,31 @@
const { ApiService } = Shopware.Classes;
export default class DumpApiService extends ApiService {
constructor(httpClient, loginService, apiEndpoint = 'aeon-dump-manager') {
super(httpClient, loginService, apiEndpoint);
}
getList() {
return this.httpClient
.get('/_action/aeon-dump-manager/dumps', { headers: this.getBasicHeaders() })
.then((response) => ApiService.handleResponse(response));
}
create() {
return this.httpClient
.post('/_action/aeon-dump-manager/dumps', {}, { headers: this.getBasicHeaders() })
.then((response) => ApiService.handleResponse(response));
}
remove(filename) {
return this.httpClient
.delete(`/_action/aeon-dump-manager/dumps/${encodeURIComponent(filename)}`, { headers: this.getBasicHeaders() })
.then((response) => ApiService.handleResponse(response));
}
getJobStatus(jobId) {
return this.httpClient
.get(`/_action/aeon-dump-manager/dumps/jobs/${encodeURIComponent(jobId)}`, { headers: this.getBasicHeaders() })
.then((response) => ApiService.handleResponse(response));
}
}