From 21f65e24803832ce3298b776d0f2dda18ab14fbc Mon Sep 17 00:00:00 2001 From: Jeroen De Meerleer Date: Tue, 10 Jan 2023 17:21:49 +0100 Subject: [PATCH] Added timed scheduled runs --- CHANGELOG.md | 3 + assets/js/job/Utils.js | 24 ++ assets/js/job/addedit.js | 34 +- assets/js/job/index.js | 144 +++++--- assets/scss/job/index.scss | 30 +- config/routes.yaml | 9 +- src/Command/DaemonCommand.php | 2 +- src/Command/RunCommand.php | 4 +- src/Controller/JobController.php | 24 +- src/Repository/JobRepository.php | 18 +- templates/job/index.html.twig | 36 +- templates/job/view.html.twig | 2 +- translations/messages.en.xlf | 60 ++- translations/messages.fr.xlf | 606 +++++++++++++++++++++++++++++++ translations/messages.leet.xlf | 68 ++-- translations/messages.lol.xlf | 542 ++++++++++++++++++++++++--- translations/messages.nl.xlf | 68 ++-- translations/security.en.xlf | 86 +++++ translations/security.fr.xlf | 86 +++++ translations/security.nl.xlf | 86 +++++ version | 2 +- 21 files changed, 1703 insertions(+), 231 deletions(-) create mode 100644 translations/messages.fr.xlf create mode 100644 translations/security.en.xlf create mode 100644 translations/security.fr.xlf create mode 100644 translations/security.nl.xlf diff --git a/CHANGELOG.md b/CHANGELOG.md index 9348c11..f0a6b9e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,7 @@ # Changelog +## Version 1.2 +### New +* Added timed scheduled runs ## Version 1.1 diff --git a/assets/js/job/Utils.js b/assets/js/job/Utils.js index 1390c09..87055a1 100644 --- a/assets/js/job/Utils.js +++ b/assets/js/job/Utils.js @@ -9,4 +9,28 @@ Utils.initTags = () => { }) } +Utils.timepickerOptions = { + localization:{ + locale: 'nl', + format: 'dd/MM/yyyy HH:mm:ss' + }, + display: { + icons: { + time: 'icon-clock-o', + date: 'icon-calendar', + up: 'icon-arrow-up', + down: 'icon-arrow-down', + previous: 'icon-chevron-left', + next: 'icon-chevron-right', + today: 'icon-calendar-check-o', + clear: 'icon-delete', + close: 'icon-x', + }, + components: { + seconds: true, + useTwentyfourHour: true + } + }, +} + export default Utils; \ No newline at end of file diff --git a/assets/js/job/addedit.js b/assets/js/job/addedit.js index 098dbda..ad189f9 100644 --- a/assets/js/job/addedit.js +++ b/assets/js/job/addedit.js @@ -1,7 +1,6 @@ import 'bootstrap'; -import moment from 'moment'; -import * as tempusDominus from '@eonasdan/tempus-dominus/dist/js/tempus-dominus'; -import customDateFormat from '@eonasdan/tempus-dominus/dist/plugins/customDateFormat' +import {TempusDominus,extend} from "@eonasdan/tempus-dominus"; +import customDateFormat from '@eonasdan/tempus-dominus/dist/plugins/customDateFormat'; import Utils from "./Utils"; document.addEventListener("readystatechange", event => { @@ -18,34 +17,11 @@ document.addEventListener("readystatechange", event => { } }); -const timepickerOptions = { - localization:{ - locale: 'nl', - format: 'dd/MM/yyyy HH:mm:ss' - }, - display: { - icons: { - time: 'icon-clock-o', - date: 'icon-calendar', - up: 'icon-arrow-up', - down: 'icon-arrow-down', - previous: 'icon-chevron-left', - next: 'icon-chevron-right', - today: 'icon-calendar-check-o', - clear: 'icon-delete', - close: 'icon-x', - }, - components: { - seconds: true, - useTwentyfourHour: true - } - }, -} function initDatePickers() { - tempusDominus.extend(customDateFormat); - new tempusDominus.TempusDominus(document.querySelector('#nextrunselector'), timepickerOptions); - new tempusDominus.TempusDominus(document.querySelector('#lastrunselector'), timepickerOptions); + extend(customDateFormat); + new TempusDominus(document.querySelector('#nextrunselector'), Utils.timepickerOptions); + new TempusDominus(document.querySelector('#lastrunselector'), Utils.timepickerOptions); } function initCronType() diff --git a/assets/js/job/index.js b/assets/js/job/index.js index 2ee08b7..899c63d 100644 --- a/assets/js/job/index.js +++ b/assets/js/job/index.js @@ -2,11 +2,14 @@ import {Modal} from 'bootstrap'; import image from '/assets/images/ajax-loader.gif' import '/assets/scss/job/index.scss'; import Utils from "./Utils"; +import customDateFormat from '@eonasdan/tempus-dominus/dist/plugins/customDateFormat'; +import {DateTime,TempusDominus,extend} from "@eonasdan/tempus-dominus"; document.addEventListener("readystatechange", event => { if(event.target.readyState === 'complete') { initDeleteButtons(); - initRunNowButtons(); + initRunButtons(); + initTimepicker(); Utils.initTags(); } }); @@ -27,55 +30,98 @@ function initDeleteButtons() { })); } -function initRunNowButtons() { - document.querySelectorAll('.runnow').forEach(elem => elem.addEventListener("click", event => { +var selecttimedatepicker; +function initTimepicker() { + extend(customDateFormat); + let modal = document.querySelector('#run_selecttime'); + let datepickeroptions = Utils.timepickerOptions; + datepickeroptions.display.inline = true; + datepickeroptions.display.sideBySide = true; + datepickeroptions.restrictions = { + minDate: new Date() + }; + selecttimedatepicker = new TempusDominus(document.querySelector('#selecttime_datepicker'), datepickeroptions); +} +function initRunButtons() { + document.querySelectorAll('.run').forEach(elem => elem.addEventListener("click", event => { let me = event.currentTarget; - let href = me.dataset.href; - - let runnowCnt = document.querySelector('.runnow-content'); - if(runnowCnt.querySelector('img') === null) { - let loaderImg = document.createElement('img'); - loaderImg.src = image; - runnowCnt.appendChild(loaderImg); + let norun = me.closest('tr').classList.contains('norun') + let maxdate = new DateTime(me.dataset.nextrun) + if (maxdate < new DateTime() ) { + if (norun) { + maxdate = undefined; + } else { + console.error('You cannot have to be run jobs in the past'); + return; + } } - document.querySelector('.container-fluid').classList.add('blur'); - document.querySelector('.runnow-overlay').classList.add('d-block'); - document.querySelector('.runnow-overlay').classList.remove('d-none'); - - fetch(href, { method: 'GET' }) - .then(response => response.json()) - .then(data => { - let modal = document.querySelector('#runnow_result'); - modal.querySelector('.modal-title').innerHTML = data.title; - if (data.status == 'deferred') { - modal.querySelector('.modal-body').innerHTML = data.message; - me.classList.add('disabled'); - let td = me.closest('td'); - td.querySelectorAll('.btn').forEach(btn => { - btn.classList.add('btn-outline-success'); - btn.classList.remove('btn-outline-primary'); - btn.classList.remove('btn-outline-danger'); - }) - - - let tr = me.closest('tr'); - tr.classList.add('running'); - tr.classList.add('text-success'); - tr.classList.remove('norun'); - tr.classList.remove('text-danger'); - } else if (data.status == 'ran') { - let content = '

' + data.message + '

' - content += '
' + data.output + '
' - - modal.querySelector('.modal-body').innerHTML = content; - } - - var bsModal = new Modal('#runnow_result').show(); - - document.querySelector('.container-fluid').classList.remove('blur'); - document.querySelector('.runnow-overlay').classList.remove('d-block'); - document.querySelector('.runnow-overlay').classList.add('d-none'); - }) + selecttimedatepicker.updateOptions({ + restrictions: { + maxDate: maxdate + } + }) + selecttimedatepicker.viewDate = new DateTime(); + var bsModal = new Modal('#run_selecttime'); + bsModal.show(); + + document.querySelectorAll('.schedule').forEach(elem => elem.addEventListener("click", event => { + bsModal.hide(); + let time = Math.floor(selecttimedatepicker.viewDate / 1000); + run(me, time); + })); + document.querySelectorAll('.run-now').forEach(elem => elem.addEventListener("click", event => { + bsModal.hide(); + run(me); + })); + })); +} +function run(elem, time = 0) { + let href = elem.dataset.href; + if (time > 0) href = href + '/' + time.toString(); + + let runCnt = document.querySelector('.run-content'); + if(runCnt.querySelector('img') === null) { + let loaderImg = document.createElement('img'); + loaderImg.src = image; + runCnt.appendChild(loaderImg); + } + document.querySelector('.container-fluid').classList.add('blur'); + document.querySelector('.run-overlay').classList.add('d-block'); + document.querySelector('.run-overlay').classList.remove('d-none'); + + fetch(href, { method: 'GET' }) + .then(response => response.json()) + .then(data => { + let modal = document.querySelector('#run_result'); + modal.querySelector('.modal-title').innerHTML = data.title; + if (data.status == 'deferred') { + modal.querySelector('.modal-body').innerHTML = data.message; + elem.classList.add('disabled'); + let td = elem.closest('td'); + td.querySelectorAll('.btn').forEach(btn => { + btn.classList.add('btn-outline-success'); + btn.classList.remove('btn-outline-primary'); + btn.classList.remove('btn-outline-danger'); + }) + + + let tr = elem.closest('tr'); + tr.classList.add('running'); + tr.classList.add('text-success'); + tr.classList.remove('norun'); + tr.classList.remove('text-danger'); + } else if (data.status == 'ran') { + let content = '

' + data.message + '

' + content += '
' + data.output + '
' + + modal.querySelector('.modal-body').innerHTML = content; + } + + var runModal = new Modal('#run_result'); + runModal.show(); + + document.querySelector('.container-fluid').classList.remove('blur'); + document.querySelector('.run-overlay').classList.remove('d-block'); + document.querySelector('.run-overlay').classList.add('d-none'); }) - ) } \ No newline at end of file diff --git a/assets/scss/job/index.scss b/assets/scss/job/index.scss index 1c29d51..f35b756 100644 --- a/assets/scss/job/index.scss +++ b/assets/scss/job/index.scss @@ -1,5 +1,7 @@ -@import "/assets/scss/base"; -@import "/assets/scss/icons"; +@import "assets/scss/base"; +@import "assets/scss/icons"; +@import "/node_modules/@eonasdan/tempus-dominus/dist/css/tempus-dominus.css"; +@import "assets/scss/tempus-dominus-dark"; tr.norun td { background-color: #f8d7da; @@ -33,8 +35,8 @@ td.status-col { filter: blur(3px); } -.runnow-overlay { - .runnow-blur { +.run-overlay { + .run-blur { bottom: 0; left: 0; position: fixed; @@ -43,7 +45,7 @@ td.status-col { z-index: 1500; } - .runnow-content { + .run-content { font-size: 10px; height: 50px; position: absolute; @@ -54,4 +56,22 @@ td.status-col { margin-left: -50px; margin-top: -50px; } +} + +#run_selecttime { + .tempus-dominus-widget { + box-shadow: none; + + &.timepicker-sbs { + width: 19em; + + .td-row { + flex-direction: column; + + .td-half { + width: 19em; + } + } + } + } } \ No newline at end of file diff --git a/config/routes.yaml b/config/routes.yaml index 832948d..9ee50f2 100644 --- a/config/routes.yaml +++ b/config/routes.yaml @@ -61,11 +61,14 @@ job_edit: requirements: id: \d+ -job_runnow: - path: '/{_locale}/job/{id}/runnow' - controller: App\Controller\JobController::runNowAction +job_run: + path: '/{_locale}/job/{id}/run/{timestamp}' + controller: App\Controller\JobController::runAction requirements: id: \d+ + timestamp: \d+ + defaults: + timestamp: 0 job_add: path: '/{_locale}/job/add' diff --git a/src/Command/DaemonCommand.php b/src/Command/DaemonCommand.php index 287019b..0cdf6ca 100644 --- a/src/Command/DaemonCommand.php +++ b/src/Command/DaemonCommand.php @@ -63,7 +63,7 @@ class DaemonCommand extends Command $consolerun = $jobRepo->getTempVar($job, 'consolerun', false); if($consolerun && !$rebootedself) continue; } - $manual = ($job->getRunning() == 2); + $manual = ($job->getRunning() > 1); $jobRepo->setJobRunning($job, true); $output->writeln('Running Job ' . $job->getId()); if($async) { diff --git a/src/Command/RunCommand.php b/src/Command/RunCommand.php index 6e2dc5c..4e3959c 100755 --- a/src/Command/RunCommand.php +++ b/src/Command/RunCommand.php @@ -47,14 +47,14 @@ class RunCommand extends Command } $jobRepo->setJobRunning($job, true); $jobRepo->setTempVar($job, 'consolerun', true); - $result = $jobRepo->runNow($job, true); + $result = $jobRepo->run($job, true); if($job->getData('crontype') == 'reboot') { $sleeping = true; while($sleeping) { if(time() >= $job->getRunning()) $sleeping = false; sleep(1); } - $result = $jobRepo->runNow($job, true); + $result = $jobRepo->run($job, true); } $jobRepo->setJobRunning($job, false); $jobRepo->setTempVar($job, 'consolerun', false); diff --git a/src/Controller/JobController.php b/src/Controller/JobController.php index 1116aa1..d78341b 100644 --- a/src/Controller/JobController.php +++ b/src/Controller/JobController.php @@ -81,30 +81,30 @@ class JobController extends AbstractController } } - public function runNowAction(Request $request, ManagerRegistry $doctrine, TranslatorInterface $translator, int $id): JsonResponse + public function runAction(Request $request, ManagerRegistry $doctrine, TranslatorInterface $translator, int $id, int $timestamp): JsonResponse { if($request->getMethod() == 'GET') { $jobRepo = $doctrine->getRepository(Job::class); $job = $jobRepo->find($id); - $runnowResult = $jobRepo->runNow($job); - if ($runnowResult['success'] === NULL) { + $runResult = $jobRepo->run($job, false, $timestamp); + if ($runResult['success'] === NULL) { $return = [ 'status' => 'deferred', 'success' => NULL, - 'title' => $translator->trans('job.index.runnow.deferred.title'), - 'message' => $translator->trans('job.index.runnow.deferred.message') + 'title' => $translator->trans('job.index.run.deferred.title'), + 'message' => $translator->trans('job.index.run.deferred.message') ]; } else { $return = [ 'status' => 'ran', - 'success' => $runnowResult['success'], - 'title' => $runnowResult['success'] ? $translator->trans('job.index.runnow.ran.title.success') : $translator->trans('job.index.runnow.ran.title.failed'), - 'message' => $translator->trans('job.index.runnow.ran.message', [ - '_runtime_' => number_format($runnowResult['runtime'], 3), - '_exitcode_' => $runnowResult['exitcode'] + 'success' => $runResult['success'], + 'title' => $runResult['success'] ? $translator->trans('job.index.run.ran.title.success') : $translator->trans('job.index.run.ran.title.failed'), + 'message' => $translator->trans('job.index.run.ran.message', [ + '_runtime_' => number_format($runResult['runtime'], 3), + '_exitcode_' => $runResult['exitcode'] ]), - 'exitcode' => $runnowResult['exitcode'], - 'output' => $runnowResult['output'], + 'exitcode' => $runResult['exitcode'], + 'output' => $runResult['output'], ]; } return new JsonResponse($return); diff --git a/src/Repository/JobRepository.php b/src/Repository/JobRepository.php index bf85027..e45e9f1 100644 --- a/src/Repository/JobRepository.php +++ b/src/Repository/JobRepository.php @@ -142,7 +142,7 @@ class JobRepository extends EntityRepository { $em = $this->getEntityManager(); - if(in_array($job->getRunning(), [0,1,2])) $job->setRunning($status ? 1 : 0); + $job->setRunning($status ? 1 : 0); $em->persist($job); $em->flush(); @@ -320,7 +320,7 @@ class JobRepository extends EntityRepository private function runRebootJob(Job &$job, float &$starttime, bool &$manual): array { $em = $this->getEntityManager(); - if($job->getRunning() == 1) { + if($this->getTempVar($job, 'rebooting', false) === false) { if(isset($_ENV['DEMO_MODE']) && $_ENV['DEMO_MODE'] == 'true') { $job->setRunning(time() + $job->getData('reboot-delay-secs') + ($job->getData('reboot-duration') * 60)); $em->persist($job); @@ -341,6 +341,7 @@ class JobRepository extends EntityRepository } $job->setRunning(time() + $job->getData('reboot-delay-secs') + ($job->getData('reboot-duration') * 60)); + $this->setTempVar($job, 'rebooting', true); $em->persist($job); $em->flush(); @@ -358,7 +359,7 @@ class JobRepository extends EntityRepository } return ['status' => 'deferred']; - } elseif($job->getRunning() != 0) { + } elseif($this->getTempVar($job, 'rebooting', false) === true) { if($job->getRunning() > time()) { return ['status' => 'deferred']; } @@ -411,11 +412,15 @@ class JobRepository extends EntityRepository * @return array * @throws \Doctrine\DBAL\Exception */ - public function runNow(Job &$job, $console = false) { + public function run(Job &$job, $console = false, int $timestamp = 0) + { $em = $this->getEntityManager(); $runRepo = $this->getEntityManager()->getRepository(Run::class); - - if($console == false && ($runRepo->isSlowJob($job)) || count($job->getRuns()) == 0 || $job->getData('crontype') === 'reboot') { + if ($timestamp > 0) { + $job->setRunning($timestamp); + $em->persist($job); + $em->flush(); + } elseif($console == false && ($runRepo->isSlowJob($job)) || count($job->getRuns()) == 0 || $job->getData('crontype') === 'reboot') { if(in_array($job->getRunning(), [0,1,2])) { $job->setRunning(2); $em->persist($job); @@ -506,7 +511,6 @@ class JobRepository extends EntityRepository } while ($nextrun < time()); $job->setNextrun($nextrun); - } $this->deleteTempVar($job); diff --git a/templates/job/index.html.twig b/templates/job/index.html.twig index b1f3d6d..c9079df 100644 --- a/templates/job/index.html.twig +++ b/templates/job/index.html.twig @@ -25,7 +25,7 @@ {{ job.interval | interval }} {{ job.nextrun | date("d/m/Y H:i:s") }} - + @@ -34,7 +34,31 @@ {% endfor %} -