From 4485be4a48184022e401e6e7ca3d3237fd98d501 Mon Sep 17 00:00:00 2001
From: Lee Peuker
Date: Tue, 27 Jun 2023 22:16:01 +0200
Subject: [PATCH 1/3] Update trakt job history UI
---
public/js/job-modal.js | 102 ++++++++++++++++++
settings/routes.php | 5 +
src/HttpController/JobController.php | 17 +++
src/HttpController/SettingsController.php | 1 -
src/JobQueue/JobEntity.php | 13 ++-
src/JobQueue/JobEntityList.php | 33 ++++++
src/JobQueue/JobQueueApi.php | 9 +-
src/JobQueue/JobQueueRepository.php | 29 +++--
src/ValueObject/JobStatus.php | 7 +-
src/ValueObject/JobType.php | 7 +-
templates/component/modal-job.html.twig | 37 +++++++
.../page/settings-integration-trakt.html.twig | 56 +++-------
12 files changed, 250 insertions(+), 66 deletions(-)
create mode 100644 public/js/job-modal.js
create mode 100644 src/JobQueue/JobEntityList.php
create mode 100644 templates/component/modal-job.html.twig
diff --git a/public/js/job-modal.js b/public/js/job-modal.js
new file mode 100644
index 00000000..782f81d8
--- /dev/null
+++ b/public/js/job-modal.js
@@ -0,0 +1,102 @@
+const jobModal = new bootstrap.Modal('#jobModal')
+const jobModalTypeInput = document.getElementById('jobModalType');
+
+async function showJobModal(jobType) {
+ jobModalTypeInput.value = jobType
+ setJobModalTitle(jobModalTypeInput.value)
+
+ setJobModalLoadingSpinner(true)
+
+ jobModal.show();
+ loadJobModalTable(await fetchJobs(jobModalTypeInput.value))
+
+ setJobModalLoadingSpinner(false)
+}
+
+function setJobModalTitle(jobType) {
+ let title
+
+ switch (jobType) {
+ case 'trakt_import_ratings':
+ title = 'Rating imports';
+ break;
+ case 'trakt_import_history':
+ title = 'History imports';
+ break;
+ default:
+ throw new Error('Not supported job type: ' + jobType);
+ }
+
+
+ document.getElementById('jobModalTitle').innerText = title;
+}
+
+async function refreshJobModal() {
+ setJobModalLoadingSpinner(true)
+
+ loadJobModalTable(await fetchJobs(jobModalTypeInput.value))
+
+ setJobModalLoadingSpinner(false)
+
+}
+
+async function fetchJobs(jobType) {
+
+ const response = await fetch('/jobs?type=' + jobType)
+
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status}`)
+ }
+
+ return await response.json()
+}
+
+function setJobModalLoadingSpinner(isActive = true) {
+ if (isActive === true) {
+ emptyJobModalTable()
+ document.getElementById('jobModalEmptyMessage').classList.add('d-none')
+
+ document.getElementById('jobModalLoadingSpinner').classList.remove('d-none');
+ } else {
+ document.getElementById('jobModalLoadingSpinner').classList.add('d-none');
+ }
+}
+
+async function loadJobModalTable(jobs) {
+ const table = document.getElementById('jobModalTable');
+
+ let tbodyRef = table.getElementsByTagName('tbody')[0];
+
+ table.getElementsByTagName('tbody').innerHtml = 'ads'
+ if (jobs.length === 0) {
+ document.getElementById('jobModalEmptyMessage').classList.remove('d-none')
+ } else {
+ document.getElementById('jobModalEmptyMessage').classList.add('d-none')
+ }
+
+ jobs.forEach((job, index, jobs) => {
+ let newRow = tbodyRef.insertRow();
+
+ const createdAtCell = newRow.insertCell();
+ createdAtCell.appendChild(document.createTextNode(job.createdAt));
+
+ const statusCell = newRow.insertCell();
+ statusCell.appendChild(document.createTextNode(job.status));
+
+ const finishedAtCell = newRow.insertCell();
+ finishedAtCell.appendChild(document.createTextNode(job.status === 'done' || job.status === 'failed' ? job.updatedAt : '-'));
+
+ if (index === jobs.length - 1) {
+ statusCell.style.borderBottom = '0'
+ createdAtCell.style.borderBottom = '0'
+ finishedAtCell.style.borderBottom = '0'
+ }
+ });
+}
+
+async function emptyJobModalTable() {
+ const table = document.getElementById('jobModalTable');
+
+ let tbodyRef = table.getElementsByTagName('tbody')[0];
+ tbodyRef.innerHTML = '';
+}
diff --git a/settings/routes.php b/settings/routes.php
index f3fea731..33a54df0 100644
--- a/settings/routes.php
+++ b/settings/routes.php
@@ -54,6 +54,11 @@
#############
# Job Queue #
#############
+ $routeCollector->addRoute(
+ 'GET',
+ '/jobs',
+ [\Movary\HttpController\JobController::class, 'getJobs'],
+ );
$routeCollector->addRoute(
'GET',
'/job-queue/purge-processed',
diff --git a/src/HttpController/JobController.php b/src/HttpController/JobController.php
index 21ede3cb..c86ea0b1 100644
--- a/src/HttpController/JobController.php
+++ b/src/HttpController/JobController.php
@@ -5,11 +5,13 @@
use Movary\Domain\User\Service\Authentication;
use Movary\JobQueue\JobQueueApi;
use Movary\Service\Letterboxd\Service\LetterboxdCsvValidator;
+use Movary\Util\Json;
use Movary\Util\SessionWrapper;
use Movary\ValueObject\Http\Header;
use Movary\ValueObject\Http\Request;
use Movary\ValueObject\Http\Response;
use Movary\ValueObject\Http\StatusCode;
+use Movary\ValueObject\JobType;
use RuntimeException;
use Twig\Environment;
@@ -24,6 +26,21 @@ public function __construct(
) {
}
+ public function getJobs(Request $request) : Response
+ {
+ if ($this->authenticationService->isUserAuthenticated() === false) {
+ return Response::createSeeOther('/');
+ }
+
+ $parameters = $request->getGetParameters();
+
+ $jobType = JobType::createFromString($parameters['type']);
+
+ $jobs = $this->jobQueueApi->find($this->authenticationService->getCurrentUserId(), $jobType);
+
+ return Response::createJson(Json::encode($jobs));
+ }
+
public function purgeAllJobs() : Response
{
if ($this->authenticationService->isUserAuthenticated() === false) {
diff --git a/src/HttpController/SettingsController.php b/src/HttpController/SettingsController.php
index 9085338e..39ab0b6b 100644
--- a/src/HttpController/SettingsController.php
+++ b/src/HttpController/SettingsController.php
@@ -479,7 +479,6 @@ public function renderTraktPage() : Response
'traktCredentialsUpdated' => $traktCredentialsUpdated,
'traktScheduleHistorySyncSuccessful' => $scheduledTraktHistoryImport,
'traktScheduleRatingsSyncSuccessful' => $scheduledTraktRatingsImport,
- 'lastTraktImportJobs' => $this->workerService->findLastTraktImportsForUser($user->getId()),
]),
);
}
diff --git a/src/JobQueue/JobEntity.php b/src/JobQueue/JobEntity.php
index a9fe0292..8954efcb 100644
--- a/src/JobQueue/JobEntity.php
+++ b/src/JobQueue/JobEntity.php
@@ -7,7 +7,7 @@
use Movary\ValueObject\JobStatus;
use Movary\ValueObject\JobType;
-class JobEntity
+class JobEntity implements \JsonSerializable
{
private function __construct(
private readonly int $id,
@@ -67,4 +67,15 @@ public function getUserId() : ?int
{
return $this->userId;
}
+
+ public function jsonSerialize() : array
+ {
+ return [
+ 'type' => $this->type,
+ 'status' => $this->getStatus(),
+ 'userId' => $this->getUserId(),
+ 'createdAt' => $this->getCreatedAt(),
+ 'updatedAt' => $this->getUpdatedAt()
+ ];
+ }
}
diff --git a/src/JobQueue/JobEntityList.php b/src/JobQueue/JobEntityList.php
new file mode 100644
index 00000000..7617ffcd
--- /dev/null
+++ b/src/JobQueue/JobEntityList.php
@@ -0,0 +1,33 @@
+add(JobEntity::createFromArray($historyEntry));
+ }
+
+ return $list;
+ }
+
+ private function add(JobEntity $dto) : void
+ {
+ $this->data[] = $dto;
+ }
+}
diff --git a/src/JobQueue/JobQueueApi.php b/src/JobQueue/JobQueueApi.php
index 6151ceb7..8cfe7231 100644
--- a/src/JobQueue/JobQueueApi.php
+++ b/src/JobQueue/JobQueueApi.php
@@ -83,9 +83,9 @@ public function fetchOldestWaitingJob() : ?JobEntity
return $this->repository->fetchOldestWaitingJob();
}
- public function findLastImdbSync() : ?DateTime
+ public function find(int $userId, JobType $jobType) : ?JobEntityList
{
- return $this->repository->findLastDateForJobByType(JobType::createImdbSync());
+ return $this->repository->find($userId, $jobType);
}
public function findLastLetterboxdImportsForUser(int $userId) : array
@@ -109,11 +109,6 @@ public function findLastTmdbSync() : ?DateTime
return $lastMovieSync->isAfter($lastPersonSync) ? $lastMovieSync : $lastPersonSync;
}
- public function findLastTraktImportsForUser(int $userId) : array
- {
- return $this->repository->findLastTraktImportsForUser($userId);
- }
-
public function purgeAllJobs() : void
{
$this->repository->purgeProcessedJobs();
diff --git a/src/JobQueue/JobQueueRepository.php b/src/JobQueue/JobQueueRepository.php
index 2e294ca1..e4bf5552 100644
--- a/src/JobQueue/JobQueueRepository.php
+++ b/src/JobQueue/JobQueueRepository.php
@@ -57,6 +57,19 @@ public function fetchOldestWaitingJob() : ?JobEntity
return JobEntity::createFromArray($data);
}
+ public function find(int $userId, JobType $jobType) : ?JobEntityList
+ {
+ $data = $this->dbConnection->fetchAllAssociative(
+ 'SELECT * FROM `job_queue` WHERE job_type = ? and user_id = ? ORDER BY `created_at` DESC LIMIT 10',
+ [
+ $jobType,
+ $userId
+ ],
+ );
+
+ return JobEntityList::createFromArray($data);
+ }
+
public function findLastDateForJobByType(JobType $jobType) : ?DateTime
{
$data = $this->dbConnection->fetchOne('SELECT created_at FROM `job_queue` WHERE job_type = ? AND job_status = ? ORDER BY created_at', [$jobType, JobStatus::createDone()]);
@@ -84,22 +97,6 @@ public function findLastLetterboxdImportsForUser(int $userId) : array
);
}
- public function findLastTraktImportsForUser(int $userId) : array
- {
- return $this->dbConnection->fetchAllAssociative(
- 'SELECT *
- FROM `job_queue`
- WHERE job_type IN (?, ?) AND user_id = ?
- ORDER BY created_at DESC
- LIMIT 10',
- [
- JobType::createTraktImportHistory(),
- JobType::createTraktImportRatings(),
- $userId,
- ],
- );
- }
-
public function purgeNotProcessedJobs() : void
{
$this->dbConnection->delete('job_queue', ['job_status' => (string)JobStatus::createWaiting()]);
diff --git a/src/ValueObject/JobStatus.php b/src/ValueObject/JobStatus.php
index 0f289e48..867df7e8 100644
--- a/src/ValueObject/JobStatus.php
+++ b/src/ValueObject/JobStatus.php
@@ -4,7 +4,7 @@
use RuntimeException;
-class JobStatus
+class JobStatus implements \JsonSerializable
{
private const STATUS_DONE = 'done';
@@ -55,4 +55,9 @@ public function __toString() : string
{
return $this->status;
}
+
+ public function jsonSerialize() : string
+ {
+ return $this->status;
+ }
}
diff --git a/src/ValueObject/JobType.php b/src/ValueObject/JobType.php
index 587de110..5c52e976 100644
--- a/src/ValueObject/JobType.php
+++ b/src/ValueObject/JobType.php
@@ -4,7 +4,7 @@
use RuntimeException;
-class JobType
+class JobType implements \JsonSerializable
{
private const TYPE_TMDB_PERSON_SYNC = 'tmdb_person_sync';
@@ -122,4 +122,9 @@ public function isOfTypeTraktImportRatings() : bool
{
return $this->type === self::TYPE_TRAKT_IMPORT_RATINGS;
}
+
+ public function jsonSerialize() : string
+ {
+ return $this->type;
+ }
}
diff --git a/templates/component/modal-job.html.twig b/templates/component/modal-job.html.twig
new file mode 100644
index 00000000..eb6e7922
--- /dev/null
+++ b/templates/component/modal-job.html.twig
@@ -0,0 +1,37 @@
+{% if loggedIn == true %}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Created
+
Status
+
Finished
+
+
+
+
+
+
No imports yet
+
+
+ Loading...
+
+
+
+
+
+
+
+{% endif %}
diff --git a/templates/page/settings-integration-trakt.html.twig b/templates/page/settings-integration-trakt.html.twig
index 0a02d68b..8389e7e1 100644
--- a/templates/page/settings-integration-trakt.html.twig
+++ b/templates/page/settings-integration-trakt.html.twig
@@ -6,6 +6,7 @@
{% block scripts %}
+
{% endblock %}
{% block body %}
@@ -72,57 +73,34 @@
The import only adds data missing in movary, it will not overwrite or remove existing data.
+ {% if traktScheduleHistorySyncSuccessful == true %}
+
+ History import scheduled
+
+
+ {% endif %}
+
+
History import
- Ratings import
+
- {% if traktScheduleHistorySyncSuccessful == true or traktScheduleRatingsSyncSuccessful == true %}
+ {% if traktScheduleRatingsSyncSuccessful == true %}