Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 39 additions & 10 deletions src/Controller/DashboardController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -25,16 +26,18 @@
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
{
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')]
Expand All @@ -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))
),
]);
}

Expand All @@ -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(),
]);
Expand All @@ -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)),
]);
}

Expand Down Expand Up @@ -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,
]);
}

Expand All @@ -133,6 +153,14 @@ public function getHistoryForScheduled(Request $request, int $id): Response
]);
}

/**
* @return array<int>
*/
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(
Expand Down Expand Up @@ -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]),
]);
}
}
62 changes: 61 additions & 1 deletion src/Gateway/JobGateway.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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')
Expand All @@ -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;
}

Expand Down Expand Up @@ -68,4 +74,58 @@ public function delete(Job $job): void
{
$this->jobRepository->delete($job->getId());
}

/**
* @param array<int> $scheduleIds
*
* @return array<int, float>
*/
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<int> $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();
}
}
1 change: 1 addition & 0 deletions src/Resources/config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions src/Resources/translations/messages.en.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
12 changes: 12 additions & 0 deletions src/Resources/translations/messages.fr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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é
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
{%- block content -%}
<div class="d-flex gap-3">
{% for count in counts %}
<div class="card">
<div class="card-body ps-3 pe-3">
<span class="card-title text-secondary fs-5">{{ ('coderhapsodie.ibexa_dataflow.dashboard.'~count.status~'.label')|trans }}</span>
<div class="card-text">
<span class="fs-1">{{ count.count }}</span>&nbsp;&nbsp;&nbsp;{{ ('coderhapsodie.ibexa_dataflow.dashboard.'~count.status~'.sub-label')|trans }}
</div>
</div>
</div>
{% endfor %}
</div>
{% include '@ibexadesign/ibexa_dataflow/parts/tab/dashboard_list.html.twig' with {
identifier: 'ibexa_dataflow_schedule_dashboard_results',
} %}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{%- block content -%}
{% set choices = [
{% set statusChoices = [
{
'value': 0,
'label': 'coderhapsodie.ibexa_dataflow.history.filter.none'|trans,
Expand All @@ -10,9 +10,18 @@
},
] %}

{% set source %}
<select id="ibexa_dataflow_history_filter" class="form-control ibexa-input">
{% for choice in choices %}
{% set statusSource %}
<select id="ibexa_dataflow_history_filter_status" class="form-control ibexa-input">
{% for choice in statusChoices %}
<option value="{{ choice.value }}">
{{ choice.label }}
</option>
{% endfor %}
</select>
{% endset %}
{% set typeSource %}
<select id="ibexa_dataflow_history_filter_type" class="form-control ibexa-input">
{% for choice in typeChoices %}
<option value="{{ choice.value }}">
{{ choice.label }}
</option>
Expand All @@ -22,32 +31,41 @@

{% set actions %}
{{ include('@ibexadesign/ui/component/dropdown/dropdown.html.twig', {
source: source,
choices: choices,
value: filter,
source: typeSource,
choices: typeChoices,
value: type,
class: 'me-2',
}) }}
{{ include('@ibexadesign/ui/component/dropdown/dropdown.html.twig', {
source: statusSource,
choices: statusChoices,
value: status,
}) }}
{% endset %}

{{ include('@ibexadesign/ibexa_dataflow/parts/tab/job_list.html.twig', {
identifier: 'ibexa_dataflow_history_results',
paginate_route: 'coderhapsodie.ibexa_dataflow.history',
paginate_params: {'filter': filter},
paginate_params: {'status': status, 'type': type},
headline: 'coderhapsodie.ibexa_dataflow.history.title'|trans,
actions: actions
}) }}

<script>
document.addEventListener('DOMContentLoaded', function () {
// Manage ajax pagination
document.getElementById('ibexa_dataflow_history_filter').addEventListener('change', function (e) {
const loading = document.getElementById('loading_ibexa_dataflow_history_results');
const results = document.getElementById('ibexa_dataflow_history_results').querySelector('.ibexa-table');
const pagination = document.getElementById('ibexa_dataflow_history_results').querySelector('.pag');

const statusSelect = document.getElementById('ibexa_dataflow_history_filter_status');
const typeSelect = document.getElementById('ibexa_dataflow_history_filter_type');

const refresh = (e) => {
e.preventDefault();
const loading = document.getElementById('loading_ibexa_dataflow_history_results');
const results = document.getElementById('ibexa_dataflow_history_results').querySelector('.ibexa-table');
const pagination = document.getElementById('ibexa_dataflow_history_results').querySelector('.pag');
loading.hidden = false;
results.innerHTML = '';
pagination.innerHTML = '';
fetch('{{ path('coderhapsodie.ibexa_dataflow.history') }}?filter=' + this.value)
fetch('{{ path('coderhapsodie.ibexa_dataflow.history') }}?status=' + encodeURIComponent(statusSelect.value) + '&type=' + encodeURIComponent(typeSelect.value))
.then((r) => r.text())
.then((content) => {
const node = document.createElement('div');
Expand All @@ -57,7 +75,11 @@
loading.hidden = true;
})
;
});
}

// Manage ajax pagination
statusSelect.addEventListener('change', refresh);
typeSelect.addEventListener('change', refresh);
})
</script>
{%- endblock -%}
Loading