diff --git a/src/Controller/DashboardController.php b/src/Controller/DashboardController.php index b48f2d6..f521629 100644 --- a/src/Controller/DashboardController.php +++ b/src/Controller/DashboardController.php @@ -8,6 +8,7 @@ use CodeRhapsodie\DataflowBundle\Entity\ScheduledDataflow; use CodeRhapsodie\DataflowBundle\ExceptionsHandler\ExceptionHandlerInterface; use CodeRhapsodie\DataflowBundle\ExceptionsHandler\NullExceptionHandler; +use CodeRhapsodie\DataflowBundle\Registry\DataflowTypeRegistryInterface; use CodeRhapsodie\IbexaDataflowBundle\CodeRhapsodieIbexaDataflowBundle; use CodeRhapsodie\IbexaDataflowBundle\Form\CreateOneshotType; use CodeRhapsodie\IbexaDataflowBundle\Form\CreateScheduledType; @@ -25,6 +26,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; +use Symfony\Contracts\Translation\TranslatorInterface; #[Route(path: '/ibexa_dataflow')] class DashboardController extends Controller @@ -32,9 +34,10 @@ class DashboardController extends Controller public function __construct( private readonly JobGateway $jobGateway, private readonly ScheduledDataflowGateway $scheduledDataflowGateway, - private readonly ExceptionHandlerInterface $exceptionHandler - ) - { + private readonly ExceptionHandlerInterface $exceptionHandler, + private readonly DataflowTypeRegistryInterface $registry, + private readonly TranslatorInterface $translator, + ) { } #[Route(path: '/', name: 'coderhapsodie.ibexa_dataflow.main')] @@ -51,8 +54,8 @@ public function main(): Response 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)) - ), + base64_encode(json_encode($data)) + ), ]); } @@ -67,8 +70,11 @@ public function repeating(Request $request): Response ]); $updateForm = $this->createForm(UpdateScheduledType::class); + $pager = $this->getPager($this->scheduledDataflowGateway->getListQueryForAdmin(), $request); + return $this->render('@ibexadesign/ibexa_dataflow/Dashboard/repeating.html.twig', [ - 'pager' => $this->getPager($this->scheduledDataflowGateway->getListQueryForAdmin(), $request), + 'pager' => $pager, + 'avg_times' => $this->jobGateway->getAverageExecutionTimes($this->extractIds($pager)), 'form' => $form->createView(), 'update_form' => $updateForm->createView(), ]); @@ -79,8 +85,11 @@ public function getRepeatingPage(Request $request): Response { $this->denyAccessUnlessGranted(new Attribute('ibexa_dataflow', 'view')); + $pager = $this->getPager($this->scheduledDataflowGateway->getListQueryForAdmin(), $request); + return $this->render('@ibexadesign/ibexa_dataflow/Dashboard/repeating.html.twig', [ - 'pager' => $this->getPager($this->scheduledDataflowGateway->getListQueryForAdmin(), $request), + 'pager' => $pager, + 'avg_times' => $this->jobGateway->getAverageExecutionTimes($this->extractIds($pager)), ]); } @@ -114,11 +123,22 @@ public function getOneshotPage(Request $request): Response public function getHistoryPage(Request $request): Response { $this->denyAccessUnlessGranted(new Attribute('ibexa_dataflow', 'view')); - $filter = (int) $request->query->get('filter', JobGateway::FILTER_NONE); + $statusFilter = $request->query->getInt('status', JobGateway::FILTER_NONE); + $typeFilter = $request->query->getString('type'); + + $typeChoices = [['value' => '', 'label' => $this->translator->trans('coderhapsodie.ibexa_dataflow.history.filter.type.all')]]; + foreach ($this->registry->listDataflowTypes() as $type) { + $typeChoices[] = [ + 'value' => $type::class, + 'label' => $type->getLabel(), + ]; + } return $this->render('@ibexadesign/ibexa_dataflow/Dashboard/history.html.twig', [ - 'pager' => $this->getPager($this->jobGateway->getListQueryForAdmin($filter), $request, Job::class), - 'filter' => $filter, + 'pager' => $this->getPager($this->jobGateway->getListQueryForAdmin($statusFilter, $typeFilter), $request, Job::class), + 'status' => $statusFilter, + 'type' => $typeFilter, + 'typeChoices' => $typeChoices, ]); } @@ -133,6 +153,14 @@ public function getHistoryForScheduled(Request $request, int $id): Response ]); } + /** + * @return array + */ + private function extractIds(Pagerfanta $pager): array + { + return array_map(fn(array $item) => (int)$item['id'], $pager->getCurrentPageResults()); + } + private function getPager(QueryBuilder $query, Request $request, string $class = null): Pagerfanta { $adapter = new ExceptionJSONDecoderAdapter( @@ -162,6 +190,7 @@ public function dashboard(): Response return $this->render('@ibexadesign/ibexa_dataflow/Dashboard/dashboard.html.twig', [ 'jobs' => $this->jobGateway->getListPendindOrRunning(), + 'counts' => $this->jobGateway->counts([Job::STATUS_PENDING, Job::STATUS_RUNNING, Job::STATUS_QUEUED]), ]); } } diff --git a/src/Gateway/JobGateway.php b/src/Gateway/JobGateway.php index 3898fa1..55b6f0e 100644 --- a/src/Gateway/JobGateway.php +++ b/src/Gateway/JobGateway.php @@ -7,6 +7,7 @@ use CodeRhapsodie\DataflowBundle\Entity\Job; use CodeRhapsodie\DataflowBundle\Gateway\JobGateway as JobGatewayDataflow; use CodeRhapsodie\DataflowBundle\Repository\JobRepository; +use Doctrine\DBAL\ArrayParameterType; use Doctrine\DBAL\Query\QueryBuilder; final readonly class JobGateway @@ -30,7 +31,7 @@ public function getOneshotListQueryForAdmin(): QueryBuilder ->addOrderBy('i.requested_date', 'DESC'); } - public function getListQueryForAdmin(int $filter): QueryBuilder + public function getListQueryForAdmin(int $filter, string $type): QueryBuilder { $qb = $this->jobRepository->createQueryBuilder('w') ->addOrderBy('w.requested_date', 'DESC') @@ -40,6 +41,11 @@ public function getListQueryForAdmin(int $filter): QueryBuilder $qb->andWhere('w.count > 0'); } + if (!empty($type)) { + $qb->andWhere('w.dataflow_type = :type') + ->setParameter('type', $type); + } + return $qb; } @@ -68,4 +74,58 @@ public function delete(Job $job): void { $this->jobRepository->delete($job->getId()); } + + /** + * @param array $scheduleIds + * + * @return array + */ + public function getAverageExecutionTimes(array $scheduleIds): array + { + if (empty($scheduleIds)) { + return []; + } + + $rows = $this->jobRepository->createQueryBuilder('j') + ->select('j.scheduled_dataflow_id', 'j.start_time', 'j.end_time') + ->andWhere('j.scheduled_dataflow_id IN (:ids)') + ->andWhere('j.start_time IS NOT NULL') + ->andWhere('j.end_time IS NOT NULL') + ->andWhere('j.status = :status') + ->setParameter('ids', $scheduleIds, ArrayParameterType::INTEGER) + ->setParameter('status', Job::STATUS_COMPLETED) + ->fetchAllAssociative(); + + $totals = []; + $counts = []; + foreach ($rows as $row) { + $id = (int) $row['scheduled_dataflow_id']; + $start = new \DateTimeImmutable($row['start_time']); + $end = new \DateTimeImmutable($row['end_time']); + $totals[$id] = ($totals[$id] ?? 0) + ($end->getTimestamp() - $start->getTimestamp()); + $counts[$id] = ($counts[$id] ?? 0) + 1; + } + + $averages = []; + foreach ($totals as $id => $total) { + $averages[$id] = $total / $counts[$id]; + } + + return $averages; + } + + /** + * @param array $status + */ + public function counts(array $status): array + { + $qb = $this->jobRepository->createQueryBuilder('w')->groupBy('w.status'); + + if (!empty($status)) { + $qb->andWhere('w.status IN (:status)') + ->setParameter('status', $status, ArrayParameterType::INTEGER); + } + + return $qb->select('w.status, COUNT(w.id) as count')->fetchAllAssociative(); + } } diff --git a/src/Resources/config/services.yaml b/src/Resources/config/services.yaml index 801c0ff..ed322e4 100644 --- a/src/Resources/config/services.yaml +++ b/src/Resources/config/services.yaml @@ -15,6 +15,7 @@ services: $jobGateway: '@CodeRhapsodie\IbexaDataflowBundle\Gateway\JobGateway' $scheduledDataflowGateway: '@CodeRhapsodie\IbexaDataflowBundle\Gateway\ScheduledDataflowGateway' $exceptionHandler: '@CodeRhapsodie\DataflowBundle\ExceptionsHandler\ExceptionHandlerInterface' + $registry: '@CodeRhapsodie\DataflowBundle\Registry\DataflowTypeRegistryInterface' CodeRhapsodie\IbexaDataflowBundle\Controller\ScheduledDataflowController: public: true diff --git a/src/Resources/translations/messages.en.yaml b/src/Resources/translations/messages.en.yaml index 12fef55..31d0c94 100644 --- a/src/Resources/translations/messages.en.yaml +++ b/src/Resources/translations/messages.en.yaml @@ -11,6 +11,7 @@ coderhapsodie.ibexa_dataflow.history: History coderhapsodie.ibexa_dataflow.workflow.list.name: Name coderhapsodie.ibexa_dataflow.workflow.list.frequency: Frequency coderhapsodie.ibexa_dataflow.workflow.list.next_execution: 'Next execution' +coderhapsodie.ibexa_dataflow.workflow.list.estimated_time: 'Estimated time' coderhapsodie.ibexa_dataflow.workflow.list.enabled: 'Enabled?' coderhapsodie.ibexa_dataflow.workflow.list.history: History coderhapsodie.ibexa_dataflow.workflow.list.edit: Edit @@ -93,3 +94,14 @@ coderhapsodie.ibexa_dataflow.dashboard: Dashboard coderhapsodie.ibexa_dataflow.dashboard.title: Running or waiting tasks coderhapsodie.ibexa_dataflow.logs.trucated: 'Logs are truncated (over 1Mo).' coderhapsodie.ibexa_dataflow.logs.download: 'Download full logs' +coderhapsodie.ibexa_dataflow.history.filter.type.all: All +coderhapsodie.ibexa_dataflow.dashboard.0.label: Pending +coderhapsodie.ibexa_dataflow.dashboard.1.label: In progress +coderhapsodie.ibexa_dataflow.dashboard.2.label: Completed +coderhapsodie.ibexa_dataflow.dashboard.3.label: Queued +coderhapsodie.ibexa_dataflow.dashboard.4.label: Crashed +coderhapsodie.ibexa_dataflow.dashboard.0.sub-label: Pending +coderhapsodie.ibexa_dataflow.dashboard.1.sub-label: Running +coderhapsodie.ibexa_dataflow.dashboard.2.sub-label: Completed +coderhapsodie.ibexa_dataflow.dashboard.3.sub-label: Queued +coderhapsodie.ibexa_dataflow.dashboard.4.sub-label: Crashed diff --git a/src/Resources/translations/messages.fr.yaml b/src/Resources/translations/messages.fr.yaml index bcd0a6a..dc1df3e 100644 --- a/src/Resources/translations/messages.fr.yaml +++ b/src/Resources/translations/messages.fr.yaml @@ -11,6 +11,7 @@ coderhapsodie.ibexa_dataflow.history: Historique coderhapsodie.ibexa_dataflow.workflow.list.name: Nom coderhapsodie.ibexa_dataflow.workflow.list.frequency: Fréquence coderhapsodie.ibexa_dataflow.workflow.list.next_execution: 'Prochaine exécution' +coderhapsodie.ibexa_dataflow.workflow.list.estimated_time: 'Temps estimé' coderhapsodie.ibexa_dataflow.workflow.list.enabled: 'Activé ?' coderhapsodie.ibexa_dataflow.workflow.list.history: Historique coderhapsodie.ibexa_dataflow.workflow.list.edit: Éditer @@ -91,3 +92,14 @@ coderhapsodie.ibexa_dataflow.dashboard: Dashboard coderhapsodie.ibexa_dataflow.dashboard.title: Tâches en cours ou en attente coderhapsodie.ibexa_dataflow.logs.trucated: 'Les logs sont tronqués (supérieur à 1Mo).' coderhapsodie.ibexa_dataflow.logs.download: 'Télécharger tous les logs' +coderhapsodie.ibexa_dataflow.history.filter.type.all: Tous +coderhapsodie.ibexa_dataflow.dashboard.0.label: En attente +coderhapsodie.ibexa_dataflow.dashboard.1.label: En cours +coderhapsodie.ibexa_dataflow.dashboard.2.label: Complété +coderhapsodie.ibexa_dataflow.dashboard.3.label: File d'attente +coderhapsodie.ibexa_dataflow.dashboard.4.label: Planté +coderhapsodie.ibexa_dataflow.dashboard.0.sub-label: En attente +coderhapsodie.ibexa_dataflow.dashboard.1.sub-label: En cours d'exécution +coderhapsodie.ibexa_dataflow.dashboard.2.sub-label: Complété +coderhapsodie.ibexa_dataflow.dashboard.3.sub-label: File d'attente +coderhapsodie.ibexa_dataflow.dashboard.4.sub-label: Planté diff --git a/src/Resources/views/themes/admin/ibexa_dataflow/Dashboard/dashboard.html.twig b/src/Resources/views/themes/admin/ibexa_dataflow/Dashboard/dashboard.html.twig index 277fdec..cceb79f 100644 --- a/src/Resources/views/themes/admin/ibexa_dataflow/Dashboard/dashboard.html.twig +++ b/src/Resources/views/themes/admin/ibexa_dataflow/Dashboard/dashboard.html.twig @@ -1,4 +1,16 @@ {%- block content -%} +
+ {% for count in counts %} +
+
+ {{ ('coderhapsodie.ibexa_dataflow.dashboard.'~count.status~'.label')|trans }} +
+ {{ count.count }}   {{ ('coderhapsodie.ibexa_dataflow.dashboard.'~count.status~'.sub-label')|trans }} +
+
+
+ {% endfor %} +
{% include '@ibexadesign/ibexa_dataflow/parts/tab/dashboard_list.html.twig' with { identifier: 'ibexa_dataflow_schedule_dashboard_results', } %} diff --git a/src/Resources/views/themes/admin/ibexa_dataflow/Dashboard/history.html.twig b/src/Resources/views/themes/admin/ibexa_dataflow/Dashboard/history.html.twig index 15e6fb3..fd43981 100644 --- a/src/Resources/views/themes/admin/ibexa_dataflow/Dashboard/history.html.twig +++ b/src/Resources/views/themes/admin/ibexa_dataflow/Dashboard/history.html.twig @@ -1,5 +1,5 @@ {%- block content -%} - {% set choices = [ + {% set statusChoices = [ { 'value': 0, 'label': 'coderhapsodie.ibexa_dataflow.history.filter.none'|trans, @@ -10,9 +10,18 @@ }, ] %} - {% set source %} - + {% for choice in statusChoices %} + + {% endfor %} + + {% endset %} + {% set typeSource %} +