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 %} + +{% 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 %} + + {% endif %} +
+ History import - Ratings import +
- {% if traktScheduleHistorySyncSuccessful == true or traktScheduleRatingsSyncSuccessful == true %} + {% if traktScheduleRatingsSyncSuccessful == true %} {% endif %} -
-
- -
Import Log
- -

List of your last trakt.tv imports (max 10).

- - - -
- - - - - - - - - - - {% for job in lastTraktImportJobs %} - - - - - - - {% endfor %} - -
TypeStatusCreatedFinished
{% if job.job_type == 'trakt_import_history' %} History {% else %} Ratings {% endif %}{{ job.job_status }}{{ job.created_at }}{% if job.job_status == 'done' %}{{ job.updated_at }}{% else %}-{% endif %}
- {% if lastTraktImportJobs|length == 0 %} - - No logged imports - - {% endif %} + + Ratings import
+ + {{ include('component/modal-job.html.twig') }} {% endblock %} From 46b4bb41621d308097e9f056f61217c24fba9fd5 Mon Sep 17 00:00:00 2001 From: Lee Peuker Date: Tue, 27 Jun 2023 22:45:17 +0200 Subject: [PATCH 2/3] Improve job modal error handling --- public/js/job-modal.js | 36 +++++++++++++++---------- templates/component/modal-job.html.twig | 6 ++++- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/public/js/job-modal.js b/public/js/job-modal.js index 782f81d8..92fd3638 100644 --- a/public/js/job-modal.js +++ b/public/js/job-modal.js @@ -5,12 +5,31 @@ async function showJobModal(jobType) { jobModalTypeInput.value = jobType setJobModalTitle(jobModalTypeInput.value) + loadJobModal(true) +} + +async function loadJobModal(showModal) { setJobModalLoadingSpinner(true) + document.getElementById('jobModalEmptyMessage').classList.add('d-none') + document.getElementById('jobModalErrorAlert').classList.add('d-none') - jobModal.show(); - loadJobModalTable(await fetchJobs(jobModalTypeInput.value)) + if (showModal === true) { + jobModal.show(); + } + + let jobs = null + + try { + jobs = await fetchJobs(jobModalTypeInput.value); + } catch (error) { + document.getElementById('jobModalErrorAlert').classList.remove('d-none') + } setJobModalLoadingSpinner(false) + + if (jobs !== null) { + renderJobModalTable(jobs) + } } function setJobModalTitle(jobType) { @@ -31,15 +50,6 @@ function setJobModalTitle(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) @@ -54,15 +64,13 @@ async function fetchJobs(jobType) { 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) { +async function renderJobModalTable(jobs) { const table = document.getElementById('jobModalTable'); let tbodyRef = table.getElementsByTagName('tbody')[0]; diff --git a/templates/component/modal-job.html.twig b/templates/component/modal-job.html.twig index eb6e7922..359d5ccb 100644 --- a/templates/component/modal-job.html.twig +++ b/templates/component/modal-job.html.twig @@ -26,10 +26,14 @@
Loading...
+ + From 0bca3901b240591a408bedda6f6ea938e0df93f8 Mon Sep 17 00:00:00 2001 From: Lee Peuker Date: Tue, 27 Jun 2023 22:56:01 +0200 Subject: [PATCH 3/3] Update letterboxd job history UI --- public/js/job-modal.js | 6 +++ src/Factory.php | 1 - src/HttpController/SettingsController.php | 2 - src/JobQueue/JobQueueApi.php | 21 -------- src/JobQueue/JobQueueRepository.php | 27 ---------- .../settings-integration-letterboxd.html.twig | 53 +++++-------------- .../page/settings-integration-trakt.html.twig | 4 +- 7 files changed, 21 insertions(+), 93 deletions(-) diff --git a/public/js/job-modal.js b/public/js/job-modal.js index 92fd3638..ed104a1a 100644 --- a/public/js/job-modal.js +++ b/public/js/job-modal.js @@ -42,6 +42,12 @@ function setJobModalTitle(jobType) { case 'trakt_import_history': title = 'History imports'; break; + case 'letterboxd_import_ratings': + title = 'Rating imports'; + break; + case 'letterboxd_import_history': + title = 'History imports'; + break; default: throw new Error('Not supported job type: ' + jobType); } diff --git a/src/Factory.php b/src/Factory.php index 8fa58616..64bfbf65 100644 --- a/src/Factory.php +++ b/src/Factory.php @@ -250,7 +250,6 @@ public static function createSettingsController(ContainerInterface $container, C { return new SettingsController( $container->get(Twig\Environment::class), - $container->get(JobQueueApi::class), $container->get(Authentication::class), $container->get(UserApi::class), $container->get(MovieApi::class), diff --git a/src/HttpController/SettingsController.php b/src/HttpController/SettingsController.php index 39ab0b6b..0307b2d7 100644 --- a/src/HttpController/SettingsController.php +++ b/src/HttpController/SettingsController.php @@ -31,7 +31,6 @@ class SettingsController { public function __construct( private readonly Environment $twig, - private readonly JobQueueApi $workerService, private readonly Authentication $authenticationService, private readonly UserApi $userApi, private readonly Movie\MovieApi $movieApi, @@ -306,7 +305,6 @@ public function renderLetterboxdPage() : Response 'letterboxdRatingsSyncSuccessful' => $letterboxdRatingsSyncSuccessful, 'letterboxdRatingsImportFileInvalid' => $letterboxdRatingsImportFileInvalid, 'letterboxdDiaryImportFileInvalid' => $letterboxdDiaryImportFileInvalid, - 'lastLetterboxdImportJobs' => $this->workerService->findLastLetterboxdImportsForUser($user->getId()), ]), ); } diff --git a/src/JobQueue/JobQueueApi.php b/src/JobQueue/JobQueueApi.php index 8cfe7231..0f779eaa 100644 --- a/src/JobQueue/JobQueueApi.php +++ b/src/JobQueue/JobQueueApi.php @@ -88,27 +88,6 @@ public function find(int $userId, JobType $jobType) : ?JobEntityList return $this->repository->find($userId, $jobType); } - public function findLastLetterboxdImportsForUser(int $userId) : array - { - return $this->repository->findLastLetterboxdImportsForUser($userId); - } - - public function findLastTmdbSync() : ?DateTime - { - $lastMovieSync = $this->repository->findLastDateForJobByType(JobType::createTmdbMovieSync()); - $lastPersonSync = $this->repository->findLastDateForJobByType(JobType::createTmdbPersonSync()); - - if ($lastMovieSync === null) { - return $lastPersonSync; - } - - if ($lastPersonSync === null) { - return $lastMovieSync; - } - - return $lastMovieSync->isAfter($lastPersonSync) ? $lastMovieSync : $lastPersonSync; - } - public function purgeAllJobs() : void { $this->repository->purgeProcessedJobs(); diff --git a/src/JobQueue/JobQueueRepository.php b/src/JobQueue/JobQueueRepository.php index e4bf5552..e25967af 100644 --- a/src/JobQueue/JobQueueRepository.php +++ b/src/JobQueue/JobQueueRepository.php @@ -70,33 +70,6 @@ public function find(int $userId, JobType $jobType) : ?JobEntityList 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()]); - - if ($data === false) { - return null; - } - - return DateTime::createFromString($data); - } - - public function findLastLetterboxdImportsForUser(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::createLetterboxdImportHistory(), - JobType::createLetterboxdImportRatings(), - $userId, - ], - ); - } - public function purgeNotProcessedJobs() : void { $this->dbConnection->delete('job_queue', ['job_status' => (string)JobStatus::createWaiting()]); diff --git a/templates/page/settings-integration-letterboxd.html.twig b/templates/page/settings-integration-letterboxd.html.twig index dd6ab516..87a564a8 100644 --- a/templates/page/settings-integration-letterboxd.html.twig +++ b/templates/page/settings-integration-letterboxd.html.twig @@ -6,6 +6,7 @@ {% block scripts %} + {% endblock %} {% block body %} @@ -34,13 +35,15 @@
-
Import Diary
+
Import History

Upload the diary.csv file

- + + + {% if letterboxdDiarySyncSuccessful == true %} {% endif %}
+
+
Import Ratings

Upload the ratings.csv file

@@ -64,7 +69,9 @@ - + + + {% if letterboxdRatingsSyncSuccessful == true %} + + {{ include('component/modal-job.html.twig') }} {% endblock %} diff --git a/templates/page/settings-integration-trakt.html.twig b/templates/page/settings-integration-trakt.html.twig index 8389e7e1..536d169e 100644 --- a/templates/page/settings-integration-trakt.html.twig +++ b/templates/page/settings-integration-trakt.html.twig @@ -83,7 +83,7 @@
History import + href="/jobs/schedule/trakt-history-sync">Import history
@@ -96,7 +96,7 @@ Ratings import + href="/jobs/schedule/trakt-ratings-sync" style="margin-top: 1rem">Import ratings