From e4db24ee8d8b9dee8d8215db9ef94d7fa90e5a2f Mon Sep 17 00:00:00 2001 From: Craig Williams Date: Wed, 31 May 2023 16:00:22 +0100 Subject: [PATCH] Added a page & table for viewing a list of installed tasks --- composer.json | 6 +- locale/en_US/messages.php | 27 +++++ routes/tasks.php | 22 ++++ src/Bakery/ScheduleCommand.php | 1 - src/Controller/TaskController.php | 110 ++++++++++++++++++++ src/Database/Seeds/SchedulerPermissions.php | 95 +++++++++++++++++ src/ServicesProvider/ServicesProvider.php | 19 +++- src/Twig/FormatCronExtension.php | 64 ++++++++++++ templates/navigation/sidebar-menu.html.twig | 10 ++ templates/pages/tasks.html.twig | 41 ++++++++ templates/tables/tasks.html.twig | 49 +++++++++ 11 files changed, 440 insertions(+), 4 deletions(-) create mode 100644 locale/en_US/messages.php create mode 100644 routes/tasks.php create mode 100644 src/Controller/TaskController.php create mode 100644 src/Database/Seeds/SchedulerPermissions.php create mode 100644 src/Twig/FormatCronExtension.php create mode 100644 templates/navigation/sidebar-menu.html.twig create mode 100644 templates/pages/tasks.html.twig create mode 100644 templates/tables/tasks.html.twig diff --git a/composer.json b/composer.json index 0405b04..d45f2e3 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,9 @@ "type": "userfrosting-sprinkle", "description": "A sprinkle to add scheduled events.", "require": { - "dragonmantank/cron-expression": "*" + "dragonmantank/cron-expression": "*", + "lorisleiva/cron-translator": "^0.3.2", + "twig/intl-extra": "^3.6" }, "autoload": { "files" : [ @@ -13,4 +15,4 @@ "UserFrosting\\Sprinkle\\Scheduler\\": "src/" } } -} \ No newline at end of file +} diff --git a/locale/en_US/messages.php b/locale/en_US/messages.php new file mode 100644 index 0000000..ed39c98 --- /dev/null +++ b/locale/en_US/messages.php @@ -0,0 +1,27 @@ + [ + 1 => 'Task', + 2 => 'Tasks', + + 'NAME' => 'Task name', + 'PAGE_DESCRIPTION' => 'This page shows a list of programmed tasks that the UserFrosting Scheduler sprinkle knows about, their schedules and when they are next expected to run.', + ], + + 'SPRINKLE' => 'Sprinkle', + 'SCHEDULE' => 'Schedule', + 'NEXT_RUN_TIME' => 'Next run time', +]; diff --git a/routes/tasks.php b/routes/tasks.php new file mode 100644 index 0000000..45976f8 --- /dev/null +++ b/routes/tasks.php @@ -0,0 +1,22 @@ +group('/tasks', function () { + $this->get('', 'UserFrosting\Sprinkle\Scheduler\Controller\TaskController:pageList') + ->setName('uri_permissions'); +})->add('authGuard')->add(new NoCache()); + +$app->group('/api/tasks', function () { + $this->get('', 'UserFrosting\Sprinkle\Scheduler\Controller\TaskController:getList'); +})->add('authGuard')->add(new NoCache()); diff --git a/src/Bakery/ScheduleCommand.php b/src/Bakery/ScheduleCommand.php index 296b169..4bf3623 100644 --- a/src/Bakery/ScheduleCommand.php +++ b/src/Bakery/ScheduleCommand.php @@ -110,6 +110,5 @@ class ScheduleCommand extends BaseCommand $this->io->error('Schedule failed !'); return self::FAILURE; } - } } diff --git a/src/Controller/TaskController.php b/src/Controller/TaskController.php new file mode 100644 index 0000000..348c5fe --- /dev/null +++ b/src/Controller/TaskController.php @@ -0,0 +1,110 @@ +getQueryParams(); + + /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ + $authorizer = $this->ci->authorizer; + + /** @var \UserFrosting\Sprinkle\Account\Database\Models\Interfaces\UserInterface $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'uri_tasks')) { + throw new ForbiddenException(); + } + + /** @var \UserFrosting\Sprinkle\Scheduler\Scheduler $scheduler */ + $scheduler = $this->ci->scheduler; + + $tasks = $scheduler->getTasks(); + + $tasks = array_map( + function($val) { + $val['schedule_nice'] = CronTranslator::translate($val['schedule']); + return $val; + }, + $tasks + ); + + // Be careful how you consume this data - it has not been escaped and contains untrusted user-supplied content. + // For example, if you plan to insert it into an HTML DOM, you must escape it on the client side (or use client-side templating). + return $response->withJson($tasks, 200, JSON_PRETTY_PRINT); + } + + /** + * Renders the task listing page. + * + * This page renders a table of tasks, with dropdown menus for admin actions for each task. + * Actions typically include: run task + * This page requires authentication. + * + * Request type: GET + * + * @param Request $request + * @param Response $response + * @param array $args + * + * @throws ForbiddenException If user is not authorized to access page + */ + public function pageList(Request $request, Response $response, $args) + { + /** @var \UserFrosting\Sprinkle\Account\Authorize\AuthorizationManager */ + $authorizer = $this->ci->authorizer; + + /** @var \UserFrosting\Sprinkle\Account\Database\Models\Interfaces\UserInterface $currentUser */ + $currentUser = $this->ci->currentUser; + + // Access-controlled page + if (!$authorizer->checkAccess($currentUser, 'uri_tasks')) { + throw new ForbiddenException(); + } + + /** @var \UserFrosting\Sprinkle\Scheduler\Scheduler $scheduler */ + $scheduler = $this->ci->scheduler; + + $tasks = $scheduler->getTasks(); + + return $this->ci->view->render($response, 'pages/tasks.html.twig', [ + 'tasks' => $tasks + ]); + } +} diff --git a/src/Database/Seeds/SchedulerPermissions.php b/src/Database/Seeds/SchedulerPermissions.php new file mode 100644 index 0000000..a09ede5 --- /dev/null +++ b/src/Database/Seeds/SchedulerPermissions.php @@ -0,0 +1,95 @@ +getPermissions(); + $this->savePermissions($permissions); + + // Add default mappings to permissions + $this->syncPermissionsRole($permissions); + } + + /** + * @return array Permissions to seed + */ + protected function getPermissions() + { + return [ + 'uri_task' => new Permission([ + 'slug' => 'uri_task', + 'name' => 'View task', + 'conditions' => 'always()', + 'description' => 'View the task page of any task.', + ]), + 'uri_tasks' => new Permission([ + 'slug' => 'uri_tasks', + 'name' => 'Tasks management page', + 'conditions' => 'always()', + 'description' => 'View a page containing a table of tasks.', + ]), + ]; + } + + /** + * Save permissions. + * + * @param array $permissions + */ + protected function savePermissions(array &$permissions) + { + foreach ($permissions as $slug => $permission) { + // Trying to find if the permission already exist + $existingPermission = Permission::where(['slug' => $permission->slug, 'conditions' => $permission->conditions])->first(); + + // Don't save if already exist, use existing permission reference + // otherwise to re-sync permissions and roles + if ($existingPermission == null) { + $permission->save(); + } else { + $permissions[$slug] = $existingPermission; + } + } + } + + /** + * Sync permissions with default roles. + * + * @param array $permissions + */ + protected function syncPermissionsRole(array $permissions) + { + $roleSiteAdmin = Role::where('slug', 'site-admin')->first(); + if ($roleSiteAdmin) { + $roleSiteAdmin->permissions()->syncWithoutDetaching([ + $permissions['uri_tasks']->id, + $permissions['uri_task']->id, + ]); + } + } +} diff --git a/src/ServicesProvider/ServicesProvider.php b/src/ServicesProvider/ServicesProvider.php index bcaef94..c999de6 100644 --- a/src/ServicesProvider/ServicesProvider.php +++ b/src/ServicesProvider/ServicesProvider.php @@ -11,8 +11,11 @@ namespace UserFrosting\Sprinkle\Scheduler\ServicesProvider; use Illuminate\Container\Container; use Psr\Container\ContainerInterface; -use UserFrosting\Sprinkle\Scheduler\Scheduler\Scheduler; +use Twig\Extra\Intl\IntlExtension; use UserFrosting\Sprinkle\Core\ServicesProvider\BaseServicesProvider; +use UserFrosting\Sprinkle\Scheduler\Scheduler\Scheduler; +use UserFrosting\Sprinkle\Scheduler\Twig\FormatCronExtension; + /** * Sheduler services provider. @@ -38,5 +41,19 @@ class ServicesProvider $container['scheduler'] = function ($c) { return new Scheduler($c); }; + + /* + * Extends the 'view' service with the HasRole extension for Twig. + * + * @return \Slim\Views\Twig + */ + $container->extend('view', function ($view, $c) { + $twig = $view->getEnvironment(); + + $twig->addExtension(new IntlExtension()); + $twig->addExtension(new FormatCronExtension($c)); + + return $view; + }); } } diff --git a/src/Twig/FormatCronExtension.php b/src/Twig/FormatCronExtension.php new file mode 100644 index 0000000..fa5da2f --- /dev/null +++ b/src/Twig/FormatCronExtension.php @@ -0,0 +1,64 @@ +services = $services; + $this->config = $services->config; + } + + public function getName() + { + return 'avsdev/schedule-formatCron'; + } + + public function getFilters() + { + return [ + new TwigFilter('formatCron', function ($cron) { + return CronTranslator::translate($cron); + }), + ]; + } + + public function getGlobals() + { + return []; + } +} diff --git a/templates/navigation/sidebar-menu.html.twig b/templates/navigation/sidebar-menu.html.twig new file mode 100644 index 0000000..520ee7e --- /dev/null +++ b/templates/navigation/sidebar-menu.html.twig @@ -0,0 +1,10 @@ +{% extends "@admin/navigation/sidebar-menu.html.twig" %} + +{% block navigation %} + {{ parent() }} + {% if checkAccess('uri_tasks') %} +
  • + {{ translate("TASK", 2) }} +
  • + {% endif %} +{% endblock %} diff --git a/templates/pages/tasks.html.twig b/templates/pages/tasks.html.twig new file mode 100644 index 0000000..f7a6eac --- /dev/null +++ b/templates/pages/tasks.html.twig @@ -0,0 +1,41 @@ +{% extends "pages/abstract/dashboard.html.twig" %} + +{% block stylesheets_page %} + + {{ assets.css('css/form-widgets') | raw }} +{% endblock %} + +{# Overrides blocks in head of base template #} +{% block page_title %}{{ translate("TASK", 2)}}{% endblock %} + +{% block page_description %}{{ translate("TASK.PAGE_DESCRIPTION")}}{% endblock %} + +{% block body_matter %} +
    +
    +
    +
    +

    {{translate('TASK', 2)}}

    +
    +
    + {% include "tables/tasks.html.twig" with { + "table" : { + "id" : "table-tasks" + } + } + %} +
    +
    +
    +
    +{% endblock %} +{% block scripts_page %} + + + + + {{ assets.js('js/form-widgets') | raw }} + +{% endblock %} diff --git a/templates/tables/tasks.html.twig b/templates/tables/tasks.html.twig new file mode 100644 index 0000000..0cacfe7 --- /dev/null +++ b/templates/tables/tasks.html.twig @@ -0,0 +1,49 @@ +{# This partial template renders a table of permissions, to be populated with rows via an AJAX request. + # This extends a generic template for paginated tables. + # + # Note that this template contains a "skeleton" table with an empty table body, and then a block of Handlebars templates which are used + # to render the table cells with the data from the AJAX request. +#} + +{% extends "tables/table-paginated.html.twig" %} + +{% block table %} + + + + + + + + + + + {% for task in tasks %} + + + + + + + + + + {% endfor %} + +
    {{translate('NAME')}}{{translate('SPRINKLE')}}{{translate('SCHEDULE')}}{{translate('NEXT_RUN_TIME')}}
    +
    + + + {{task.name}} + +
    +
    + {{task.sprinkle}} + + ({{task.schedule}}) {{task.schedule | formatCron()}} + + {{ task.next_run | format_datetime(pattern='HH:mm:ss dd/MM/yyyy') }} +
    +{% endblock %} + +{% block table_pager_controls %}{% endblock %} \ No newline at end of file