5 Commits
v5.0.0 ... v5.x

Author SHA1 Message Date
loic
6ece645267 Added Dashboard tab 2025-07-04 08:41:40 +02:00
loic
5180108598 Added Dashboard tab 2025-07-04 08:19:00 +02:00
jb cr
08cd3eccd6 prepare publishing 2025-04-02 11:18:21 +02:00
jbcr
9dc90bb001 add make by mention (#6) 2025-04-02 11:15:36 +02:00
AUDUL
4b8554c64a Add oneshot v5 (#3)
* Added possibility to create one shot job from scheduled job
2025-02-18 14:30:40 +01:00
14 changed files with 247 additions and 12 deletions

View File

@@ -1,3 +1,15 @@
# Version 5.2.1
* Fixed datepicker in oneshot modal
# Version 5.2.0
* Added Dashboard tab
# Version 5.1.1
* Add branding label
# Version 5.1.0
* Added possibility to create one shot job from scheduled job
# Version 5.0.0
* Renamed bundle to IbexaDataflowBundle

View File

@@ -13,6 +13,9 @@ use Symfony\Component\HttpKernel\Bundle\Bundle;
class CodeRhapsodieIbexaDataflowBundle extends Bundle
{
public const VERSION = '5.2.0';
public const PRODUCT_NAME = 'ibexadataflow';
protected $name = 'CodeRhapsodieIbexaDataflowBundle';
public function getContainerExtension()

View File

@@ -6,6 +6,7 @@ namespace CodeRhapsodie\IbexaDataflowBundle\Controller;
use CodeRhapsodie\DataflowBundle\Entity\Job;
use CodeRhapsodie\DataflowBundle\Entity\ScheduledDataflow;
use CodeRhapsodie\IbexaDataflowBundle\CodeRhapsodieIbexaDataflowBundle;
use CodeRhapsodie\IbexaDataflowBundle\Form\CreateOneshotType;
use CodeRhapsodie\IbexaDataflowBundle\Form\CreateScheduledType;
use CodeRhapsodie\IbexaDataflowBundle\Form\UpdateScheduledType;
@@ -14,6 +15,7 @@ use CodeRhapsodie\IbexaDataflowBundle\Gateway\JobGateway;
use CodeRhapsodie\IbexaDataflowBundle\Gateway\ScheduledDataflowGateway;
use Doctrine\DBAL\Query\QueryBuilder;
use Ibexa\Contracts\AdminUi\Controller\Controller;
use Ibexa\Contracts\Core\Ibexa;
use Ibexa\Core\MVC\Symfony\Security\Authorization\Attribute;
use Pagerfanta\Doctrine\DBAL\QueryAdapter;
use Pagerfanta\Pagerfanta;
@@ -33,7 +35,18 @@ class DashboardController extends Controller
{
$this->denyAccessUnlessGranted(new Attribute('ibexa_dataflow', 'view'));
return $this->render('@ibexadesign/ibexa_dataflow/Dashboard/main.html.twig');
$data = [
'product' => CodeRhapsodieIbexaDataflowBundle::PRODUCT_NAME,
'version' => CodeRhapsodieIbexaDataflowBundle::VERSION,
'php' => PHP_VERSION,
'ibexa' => Ibexa::VERSION,
];
return $this->render('@ibexadesign/ibexa_dataflow/Dashboard/main.html.twig', [
'link' => 'https://www.code-rhapsodie.fr/product/redirect/'.str_replace('=', '',
base64_encode(json_encode($data))
),
]);
}
public function repeating(Request $request): Response
@@ -117,7 +130,7 @@ class DashboardController extends Controller
{
$pager = new Pagerfanta(
new ExceptionJSONDecoderAdapter(
new QueryAdapter($query, fn($queryBuilder) => $queryBuilder->select('COUNT(DISTINCT id) AS total_results')
new QueryAdapter($query, fn ($queryBuilder) => $queryBuilder->select('COUNT(DISTINCT id) AS total_results')
->resetQueryPart('orderBy')
->setMaxResults(1))
)
@@ -127,4 +140,13 @@ class DashboardController extends Controller
return $pager;
}
public function dashboard(): Response
{
$this->denyAccessUnlessGranted(new Attribute('ibexa_dataflow', 'view'));
return $this->render('@ibexadesign/ibexa_dataflow/Dashboard/dashboard.html.twig', [
'jobs' => $this->jobGateway->getListPendindOrRunning(),
]);
}
}

View File

@@ -7,12 +7,14 @@ namespace CodeRhapsodie\IbexaDataflowBundle\Controller;
use CodeRhapsodie\DataflowBundle\Entity\Job;
use CodeRhapsodie\IbexaDataflowBundle\Form\CreateOneshotType;
use CodeRhapsodie\IbexaDataflowBundle\Gateway\JobGateway;
use CodeRhapsodie\IbexaDataflowBundle\Gateway\ScheduledDataflowGateway;
use Ibexa\Contracts\AdminUi\Controller\Controller;
use Ibexa\Contracts\AdminUi\Notification\NotificationHandlerInterface;
use Ibexa\Core\MVC\Symfony\Security\Authorization\Attribute;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
@@ -20,7 +22,7 @@ use Symfony\Contracts\Translation\TranslatorInterface;
#[Route(path: '/ibexa_dataflow/job')]
class JobController extends Controller
{
public function __construct(private readonly JobGateway $jobGateway, private readonly NotificationHandlerInterface $notificationHandler, private readonly TranslatorInterface $translator)
public function __construct(private readonly JobGateway $jobGateway, private readonly NotificationHandlerInterface $notificationHandler, private readonly TranslatorInterface $translator, private readonly ScheduledDataflowGateway $scheduledDataflowGateway)
{
}
@@ -81,4 +83,35 @@ class JobController extends Controller
]),
]);
}
#[Route(path: '/run-oneshot/{id}', name: 'coderhapsodie.ibexa_dataflow.job.run-oneshot', methods: ['GET'])]
public function runOneShot(int $id): Response
{
$this->denyAccessUnlessGranted(new Attribute('ibexa_dataflow', 'view'));
$scheduledDataflow = $this->scheduledDataflowGateway->find($id);
if ($scheduledDataflow === null) {
throw new NotFoundHttpException();
}
$newOneshotJob = new Job();
$newOneshotJob->setOptions($scheduledDataflow->getOptions());
$newOneshotJob->setLabel("Manual " . $scheduledDataflow->getLabel());
$newOneshotJob->setScheduledDataflowId($scheduledDataflow->getId());
$newOneshotJob->setRequestedDate((new \DateTime())->add(new \DateInterval('PT1H')));
$newOneshotJob->setDataflowType($scheduledDataflow->getDataflowType());
$form = $this->createForm(CreateOneshotType::class, $newOneshotJob, [
'action' => $this->generateUrl('coderhapsodie.ibexa_dataflow.job.create'),
]);
return new JsonResponse([
'form' => $this->renderView('@ibexadesign/ibexa_dataflow/parts/form_modal.html.twig', [
'form' => $form->createView(),
'id' => 'modal-new-oneshot',
'mode' => 'oneshot',
]),
]);
}
}

View File

@@ -54,5 +54,15 @@ final class JobGateway
{
$this->jobRepository->save($job);
}
public function getListPendindOrRunning(): array
{
$qb = $this->jobRepository->createQueryBuilder('w');
return $qb->andWhere($qb->expr()->in('w.status', ':status'))
->setParameter('status', implode(',',[Job::STATUS_RUNNING, Job::STATUS_PENDING, Job::STATUS_QUEUED]))
->orderBy('w.requested_date', 'ASC')
->execute()
->fetchAllAssociative();
}
}
class_alias(JobGateway::class, 'CodeRhapsodie\EzDataflowBundle\Gateway\JobGateway');

View File

@@ -38,6 +38,7 @@ services:
$jobGateway: '@CodeRhapsodie\IbexaDataflowBundle\Gateway\JobGateway'
$notificationHandler: '@Ibexa\Contracts\AdminUi\Notification\NotificationHandlerInterface'
$translator: '@translator'
$scheduledDataflowGateway: '@CodeRhapsodie\IbexaDataflowBundle\Gateway\ScheduledDataflowGateway'
calls:
- [ 'setContainer', [ '@service_container' ] ]
- [ 'performAccessCheck', [ ] ]
@@ -128,6 +129,14 @@ services:
arguments:
$jobRepository: '@CodeRhapsodie\DataflowBundle\Repository\JobRepository'
CodeRhapsodie\IbexaDataflowBundle\Tab\DashboardTab:
parent: Ibexa\Contracts\AdminUi\Tab\AbstractTab
public: false
arguments:
$httpKernelRuntime: '@twig.runtime.httpkernel'
tags:
- { name: ibexa.admin_ui.tab, group: coderhapsodie-ibexa_dataflow }
CodeRhapsodie\IbexaDataflowBundle\Tab\RepeatingTab:
parent: Ibexa\Contracts\AdminUi\Tab\AbstractTab
public: false

View File

@@ -16,6 +16,7 @@ coderhapsodie.ibexa_dataflow.workflow.list.history: History
coderhapsodie.ibexa_dataflow.workflow.list.edit: Edit
coderhapsodie.ibexa_dataflow.workflow.list.disable: Disable
coderhapsodie.ibexa_dataflow.workflow.list.enable: Enable
coderhapsodie.ibexa_dataflow.workflow.list.runonce: Run now
coderhapsodie.ibexa_dataflow.history.title: History
coderhapsodie.ibexa_dataflow.history.list.title: 'Executions list'
coderhapsodie.ibexa_dataflow.history.list.name: Name
@@ -82,3 +83,7 @@ coderhapsodie.dataflow.update.next: 'Next execution'
coderhapsodie.ibexa_dataflow.workflow.edit.success: 'Dataflow schedule successfully updated.'
coderhapsodie.ibexa_dataflow.workflow.edit.error: 'An error occurred during the dataflow schedule update: "%message%".'
coderhapsodie.ibexa_dataflow.notfound: 'Requested data is not found'
coderhapsodie.ibexa_dataflow.powered_by: 'Powered by'
coderhapsodie.ibexa_dataflow.made_by: 'Made by'
coderhapsodie.ibexa_dataflow.dashboard: Dashboard
coderhapsodie.ibexa_dataflow.dashboard.title: Running or waiting tasks

View File

@@ -16,6 +16,7 @@ coderhapsodie.ibexa_dataflow.workflow.list.history: Historique
coderhapsodie.ibexa_dataflow.workflow.list.edit: Éditer
coderhapsodie.ibexa_dataflow.workflow.list.disable: Désactiver
coderhapsodie.ibexa_dataflow.workflow.list.enable: Activer
coderhapsodie.ibexa_dataflow.workflow.list.runonce: Lancer maintenant
coderhapsodie.ibexa_dataflow.history.title: Historique
coderhapsodie.ibexa_dataflow.history.list.title: 'Liste des exécutions'
coderhapsodie.ibexa_dataflow.history.list.name: Nom
@@ -80,3 +81,7 @@ coderhapsodie.dataflow.update.next: 'Prochaine exécution'
coderhapsodie.ibexa_dataflow.workflow.edit.success: 'La programmation du dataflow a été mise à jour avec succès.'
coderhapsodie.ibexa_dataflow.workflow.edit.error: 'Une erreur est survenue lors de la modification de la programmation du dataflow : "%message%".'
coderhapsodie.ibexa_dataflow.notfound: 'Les données demandées sont introuvables'
coderhapsodie.ibexa_dataflow.powered_by: 'Propulsé par'
coderhapsodie.ibexa_dataflow.made_by: 'Fabriqué par'
coderhapsodie.ibexa_dataflow.dashboard: Dashboard
coderhapsodie.ibexa_dataflow.dashboard.title: Tâches en cours ou en attente

View File

@@ -0,0 +1,5 @@
{%- block content -%}
{% include '@ibexadesign/ibexa_dataflow/parts/tab/dashboard_list.html.twig' with {
identifier: 'ibexa_dataflow_schedule_dashboard_results',
} %}
{%- endblock -%}

File diff suppressed because one or more lines are too long

View File

@@ -1,10 +1,11 @@
{%- block content -%}
{% set actions %}
<button
type="button"
class="btn ibexa-btn ibexa-btn--tertiary ibexa-btn--small"
data-bs-toggle="modal"
data-bs-target="#modal-new-oneshot"
id="create-oneshot-button"
type="button"
class="btn ibexa-btn ibexa-btn--tertiary ibexa-btn--small"
data-bs-toggle="modal"
data-bs-target="#modal-new-oneshot"
>
<svg class="ibexa-icon ibexa-icon--small ibexa-icon--create">
<use xlink:href="{{ ibexa_icon_path('create') }}"></use>
@@ -53,6 +54,16 @@
})
;
});
const createButton = document.getElementById('create-oneshot-button')
if (createButton) {
createButton.addEventListener('click', () => {
const oneShotModal = document.getElementById('modal-new-oneshot');
oneShotModal.querySelector('#create_oneshot_label').value = '';
oneShotModal.querySelector('#create_oneshot_options').value = '';
oneShotModal.querySelector('.flatpickr.flatpickr-input').parentNode.parentNode.parentNode.ibexaInstance.flatpickrInstance.setDate(new Date(), true);
});
}
});
</script>
{%- endblock -%}

View File

@@ -0,0 +1,42 @@
{% set id = identifier|default('ibexa_dataflow_history_results') %}
{% import '@ibexadesign/ibexa_dataflow/macros.twig' as macros %}
<div id="loading_{{ id }}" class="text-center" hidden>
<svg class="ez-icon ez-icon--extra-large">
<use xmlns:xlink="http://www.w3.org/1999/xlink"
xlink:href="{{ ibexa_icon_path('spinner') }}"></use>
</svg>
</div>
<div id="{{ id }}" data-loader="loading_{{ id }}" class="history-details-aware">
{% set body_rows = [] %}
{% for job in jobs %}
{% set body_row_cols = [] %}
{% set body_row_cols = body_row_cols|merge([
{content: job.label},
{content: date(job.requested_date)|ibexa_short_datetime},
{content: job.start_time ? date(job.start_time)|ibexa_short_datetime : '—'},
{content: macros.translateStatus(job.status)},
]) %}
{% set body_rows = body_rows|merge([{ cols: body_row_cols }]) %}
{% endfor %}
{% embed '@ibexadesign/ui/component/table/table.html.twig' with {
headline: 'coderhapsodie.ibexa_dataflow.dashboard.title'|trans,
head_cols: [
{ content: 'coderhapsodie.ibexa_dataflow.history.list.name'|trans },
{ content: 'coderhapsodie.ibexa_dataflow.history.list.request'|trans },
{ content: 'coderhapsodie.ibexa_dataflow.history.list.start'|trans },
{ content: 'coderhapsodie.ibexa_dataflow.history.list.status'|trans },
{ },
],
body_rows,
empty_table_info_text: 'coderhapsodie.ibexa_dataflow.history.list.empty'|trans,
} %}
{% endembed %}
</div>

View File

@@ -40,6 +40,15 @@
<use xlink:href="{{ ibexa_icon_path('edit') }}"></use>
</svg>
</a>
<button
class="btn ibexa-btn ibexa-btn--ghost run-oneshot ibexa-btn--no-text"
data-url="{{ path('coderhapsodie.ibexa_dataflow.job.run-oneshot', {id: item.id}) }}"
title="{{ 'coderhapsodie.ibexa_dataflow.workflow.list.runonce'|trans }}"
>
<svg class="ibexa-icon ibexa-icon--small ibexa-icon--create">
<use xlink:href="{{ ibexa_icon_path('create') }}"></use>
</svg>
</button>
{% if item.enabled %}
<a href="{{ path('coderhapsodie.ibexa_dataflow.workflow.disable', {id: item.id}) }}"
class="btn ibexa-btn ibexa-btn--ghost ibexa-btn--no-text"
@@ -96,10 +105,10 @@
{% embed '@ibexadesign/ui/component/table/table_header.html.twig' %}
{% block actions %}
<button
type="button"
class="btn ibexa-btn ibexa-btn--tertiary ibexa-btn--small"
data-bs-toggle="modal"
data-bs-target="#modal-new-scheduled"
type="button"
class="btn ibexa-btn ibexa-btn--tertiary ibexa-btn--small"
data-bs-toggle="modal"
data-bs-target="#modal-new-scheduled"
>
<svg class="ibexa-icon ibexa-icon--small ibexa-icon--create">
<use xlink:href="{{ ibexa_icon_path('create') }}"></use>
@@ -125,3 +134,27 @@
{% endif %}
</div>
</div>
<script>
document.querySelectorAll(".run-oneshot").forEach((el) => {
el.addEventListener('click', () => {
const oneShotModal = document.getElementById('modal-new-oneshot');
fetch(el.getAttribute('data-url'))
.then(response => response.json())
.then(result => {
const node = document.createElement('div');
node.innerHTML = result.form;
oneShotModal.querySelector('#create_oneshot_label').value = node.querySelector('#create_oneshot_label').value;
oneShotModal.querySelector('#create_oneshot_options').value = node.querySelector('#create_oneshot_options').value;
oneShotModal.querySelector('.ibexa-dropdown').ibexaInstance.selectOption(node.querySelector('#create_oneshot_dataflowType').value)
oneShotModal.querySelector('.flatpickr.flatpickr-input').parentNode.parentNode.parentNode.ibexaInstance.flatpickrInstance.setDate(new Date(), true);
})
.then(() => {
bootstrap.Tab.getOrCreateInstance(document.querySelector('#ibexa-tab-label-coderhapsodie-ibexa_dataflow-code-rhapsodie-ibexa_dataflow-oneshot')).show()
bootstrap.Modal.getOrCreateInstance(oneShotModal).show()
})
});
});
</script>

45
src/Tab/DashboardTab.php Normal file
View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace CodeRhapsodie\IbexaDataflowBundle\Tab;
use CodeRhapsodie\IbexaDataflowBundle\Controller\DashboardController;
use Ibexa\Contracts\AdminUi\Tab\AbstractControllerBasedTab;
use Ibexa\Contracts\AdminUi\Tab\OrderedTabInterface;
use Symfony\Component\HttpKernel\Controller\ControllerReference;
class DashboardTab extends AbstractControllerBasedTab implements OrderedTabInterface
{
/**
* {@inheritdoc}
*/
public function getControllerReference(array $parameters): ControllerReference
{
return new ControllerReference(DashboardController::class.'::dashboard');
}
/**
* {@inheritdoc}
*/
public function getOrder(): int
{
return 0;
}
/**
* {@inheritdoc}
*/
public function getIdentifier(): string
{
return 'code-rhapsodie-ibexa_dataflow-dashboard';
}
/**
* {@inheritdoc}
*/
public function getName(): string
{
return $this->translator->trans('coderhapsodie.ibexa_dataflow.dashboard');
}
}