diff --git a/composer.json b/composer.json index b0d8c2b..b1a257f 100644 --- a/composer.json +++ b/composer.json @@ -20,6 +20,7 @@ "illuminate/bus": "^9.0", "lorisleiva/cron-translator": "^0.3.0", "nesbot/carbon": "^2.41.3", + "nunomaduro/termwind": "^1.9", "spatie/laravel-package-tools": "^1.9" }, "require-dev": { diff --git a/resources/views/.gitkeep b/resources/views/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/resources/views/alert.blade.php b/resources/views/alert.blade.php new file mode 100644 index 0000000..7a2bf24 --- /dev/null +++ b/resources/views/alert.blade.php @@ -0,0 +1,3 @@ +
+ {!! $message !!} +
diff --git a/resources/views/components/duplicate-tasks.blade.php b/resources/views/components/duplicate-tasks.blade.php new file mode 100644 index 0000000..b24bc16 --- /dev/null +++ b/resources/views/components/duplicate-tasks.blade.php @@ -0,0 +1,16 @@ +@props(['tasks']) +
+ Duplicate Tasks + +
These tasks could not be monitored because they have a duplicate name.
+ +
+ @foreach ($tasks as $task) + + @endforeach +
+ +
+ To monitor these tasks you should add ->monitorName() in the schedule to manually specify a unique name. +
+
diff --git a/resources/views/components/monitored-tasks.blade.php b/resources/views/components/monitored-tasks.blade.php new file mode 100644 index 0000000..63de8db --- /dev/null +++ b/resources/views/components/monitored-tasks.blade.php @@ -0,0 +1,63 @@ +@props(['tasks', 'dateFormat', 'usingOhDear']) +
+ Monitored Tasks + +
+ @forelse ($tasks as $task) +
+ +
+
+ + ⇁ Started at: + + {{ optional($task->lastRunStartedAt())->format($dateFormat) ?? '--' }} + + + + ⇁ Finished at: + + {{ optional($task->lastRunFinishedAt())->format($dateFormat) ?? '--' }} + + +
+ + ⇁ Failed at: + + {{ optional($task->lastRunFailedAt())->format($dateFormat) ?? '--' }} + + + + + ⇁ Next run: + {{ $task->nextRunAt()->format($dateFormat) }} + +
+ + ⇁ Grace time: + {{ $task->graceTimeInMinutes() }} minutes + + @if ($usingOhDear) + + ⇁ Registered at Oh Dear: + @if ($task->isBeingMonitoredAtOhDear()) + Yes + @else + No + @endif + + @endif +
+
+
+ @empty +
There currently are no tasks being monitored!
+ @endforelse +
+ @if ($usingOhDear) +
+ Some tasks are not registered on oh dear. You will not be notified when they do not run on time.
+ Run php artisan schedule-monitor:sync to register them and receive notifications. +
+ @endif +
diff --git a/resources/views/components/ready-for-monitoring-tasks.blade.php b/resources/views/components/ready-for-monitoring-tasks.blade.php new file mode 100644 index 0000000..7bc8fab --- /dev/null +++ b/resources/views/components/ready-for-monitoring-tasks.blade.php @@ -0,0 +1,12 @@ +@props(['tasks']) +
+ Run sync to start monitoring + +
These tasks will be monitored after running: php artisan schedule-monitor:sync
+ +
+ @foreach ($tasks as $task) + + @endforeach +
+
diff --git a/resources/views/components/task.blade.php b/resources/views/components/task.blade.php new file mode 100644 index 0000000..266c7dc --- /dev/null +++ b/resources/views/components/task.blade.php @@ -0,0 +1,15 @@ +@props(['task']) +
+ @if ($task->name()) + {{ $task->name() }} + ({{ $task->type() }}) + @else + {{ $task->type() }} + @endif + + {{ str_repeat('.', (new \Termwind\Terminal)->width() - ( + strlen($task->name() . $task->type() . $task->humanReadableCron()) + ($task->name() && $task->type() ? 9 : 6) + )) }} + + {{ $task->humanReadableCron() }} +
diff --git a/resources/views/components/title.blade.php b/resources/views/components/title.blade.php new file mode 100644 index 0000000..7629362 --- /dev/null +++ b/resources/views/components/title.blade.php @@ -0,0 +1,3 @@ +
+ {{ $slot }} +
diff --git a/resources/views/components/unnamed-tasks.blade.php b/resources/views/components/unnamed-tasks.blade.php new file mode 100644 index 0000000..7022342 --- /dev/null +++ b/resources/views/components/unnamed-tasks.blade.php @@ -0,0 +1,14 @@ +@props(['tasks']) +
+ Unnamed Tasks + +
These tasks cannot be monitored because no name could be determined for them.
+ +
+ @foreach ($tasks as $task) + + @endforeach +
+ +
To monitor these tasks you should add ->monitorName() in the schedule to manually specify a name.
+
diff --git a/resources/views/list.blade.php b/resources/views/list.blade.php new file mode 100644 index 0000000..ba95681 --- /dev/null +++ b/resources/views/list.blade.php @@ -0,0 +1,22 @@ +
+ + @if (! $readyForMonitoringTasks->isEmpty()) + + @endif + @if (! $unnamedTasks->isEmpty()) + + @endif + @if (! $duplicateTasks->isEmpty()) + + @endif +
diff --git a/resources/views/sync.blade.php b/resources/views/sync.blade.php new file mode 100644 index 0000000..9902ee7 --- /dev/null +++ b/resources/views/sync.blade.php @@ -0,0 +1,4 @@ +
+
All done! Now monitoring {{ $monitoredScheduledTasksCount }} {{ str()->plural('scheduled task', $monitoredScheduledTasksCount) }}.
+
Run php artisan schedule-monitor:list to see which jobs are now monitored.
+
diff --git a/src/Commands/ListCommand.php b/src/Commands/ListCommand.php index 4768fa3..bfbaaa9 100644 --- a/src/Commands/ListCommand.php +++ b/src/Commands/ListCommand.php @@ -3,10 +3,10 @@ namespace Spatie\ScheduleMonitor\Commands; use Illuminate\Console\Command; -use Spatie\ScheduleMonitor\Commands\Tables\DuplicateTasksTable; -use Spatie\ScheduleMonitor\Commands\Tables\MonitoredTasksTable; -use Spatie\ScheduleMonitor\Commands\Tables\ReadyForMonitoringTasksTable; -use Spatie\ScheduleMonitor\Commands\Tables\UnnamedTasksTable; +use Spatie\ScheduleMonitor\Support\ScheduledTasks\ScheduledTasks; +use Spatie\ScheduleMonitor\Support\ScheduledTasks\Tasks\Task; +use function Termwind\render; +use function Termwind\style; class ListCommand extends Command { @@ -16,11 +16,33 @@ class ListCommand extends Command public function handle() { - (new MonitoredTasksTable($this))->render(); - (new ReadyForMonitoringTasksTable($this))->render(); - (new UnnamedTasksTable($this))->render(); - (new DuplicateTasksTable($this))->render(); + $dateFormat = config('schedule-monitor.date_format'); + style('date-width')->apply('w-' . strlen(date($dateFormat))); - $this->line(''); + render(view('schedule-monitor::list', [ + 'monitoredTasks' => ScheduledTasks::createForSchedule()->monitoredTasks(), + 'readyForMonitoringTasks' => ScheduledTasks::createForSchedule()->readyForMonitoringTasks(), + 'unnamedTasks' => ScheduledTasks::createForSchedule()->unnamedTasks(), + 'duplicateTasks' => ScheduledTasks::createForSchedule()->duplicateTasks(), + 'usingOhDear' => $this->usingOhDear(), + 'dateFormat' => $dateFormat, + ])); + } + + protected function usingOhDear(): bool + { + if (! class_exists(OhDear::class)) { + return false; + } + + if (empty(config('schedule-monitor.oh_dear.api_token'))) { + return false; + } + + if (empty(config('schedule-monitor.oh_dear.site_id'))) { + return false; + } + + return true; } } diff --git a/src/Commands/SyncCommand.php b/src/Commands/SyncCommand.php index e4fd5f6..503a835 100644 --- a/src/Commands/SyncCommand.php +++ b/src/Commands/SyncCommand.php @@ -10,6 +10,7 @@ use Spatie\ScheduleMonitor\Support\Concerns\UsesScheduleMonitoringModels; use Spatie\ScheduleMonitor\Support\ScheduledTasks\ScheduledTasks; use Spatie\ScheduleMonitor\Support\ScheduledTasks\Tasks\Task; +use function Termwind\render; class SyncCommand extends Command { @@ -21,22 +22,27 @@ class SyncCommand extends Command public function handle() { - $this->info('Start syncing schedule...' . PHP_EOL); + render(view('schedule-monitor::alert', [ + 'message' => 'Start syncing schedule...', + 'class' => 'text-green' + ])); + $this ->syncScheduledTasksWithDatabase() ->syncMonitoredScheduledTaskWithOhDear(); $monitoredScheduledTasksCount = $this->getMonitoredScheduleTaskModel()->count(); - $this->info(''); - $this->info('All done! Now monitoring ' . $monitoredScheduledTasksCount . ' ' . Str::plural('scheduled task', $monitoredScheduledTasksCount) . '.'); - $this->info(''); - $this->info('Run `php artisan schedule-monitor:list` to see which jobs are now monitored.'); + render(view('schedule-monitor::sync', [ + 'monitoredScheduledTasksCount' => $monitoredScheduledTasksCount, + ])); } protected function syncScheduledTasksWithDatabase(): self { - $this->comment('Start syncing schedule with database...'); + render(view('schedule-monitor::alert', [ + 'message' => 'Start syncing schedule with database...', + ])); $monitoredScheduledTasks = ScheduledTasks::createForSchedule() ->uniqueTasks() @@ -68,12 +74,25 @@ protected function syncMonitoredScheduledTaskWithOhDear(): self $siteId = config('schedule-monitor.oh_dear.site_id'); if (! $siteId) { - $this->warn('Not syncing schedule with Oh Dear because not `site_id` is not set in the `oh-dear` config file. Learn how to set this up at https://ohdear.app/docs/general/cron-job-monitoring/php#cron-monitoring-in-laravel-php'); + render(view('schedule-monitor::alert', [ + 'message' => << + Not syncing schedule with oh dear because not site_id + is not set in the oh-dear config file. + +
+ Learn how to set this up at https://ohdear.app/docs/general/cron-job-monitoring/php#cron-monitoring-in-laravel-php. +
+ HTML, + 'class' => 'text-yellow', + ])); return $this; } - $this->comment('Start syncing schedule with Oh Dear...'); + render(view('schedule-monitor::alert', [ + 'message' => 'Start syncing schedule with Oh Dear...', + ])); $monitoredScheduledTasks = $this->getMonitoredScheduleTaskModel()->get(); @@ -91,7 +110,11 @@ protected function syncMonitoredScheduledTaskWithOhDear(): self ->toArray(); $cronChecks = app(OhDear::class)->site($siteId)->syncCronChecks($cronChecks); - $this->comment('Successfully synced schedule with Oh Dear!'); + + render(view('schedule-monitor::alert', [ + 'message' => 'Successfully synced schedule with Oh Dear!', + 'class' => 'text-green', + ])); collect($cronChecks) ->each( diff --git a/src/Commands/Tables/DuplicateTasksTable.php b/src/Commands/Tables/DuplicateTasksTable.php deleted file mode 100644 index ea327ba..0000000 --- a/src/Commands/Tables/DuplicateTasksTable.php +++ /dev/null @@ -1,38 +0,0 @@ -duplicateTasks(); - - if ($duplicateTasks->isEmpty()) { - return; - } - - $this->command->line(''); - $this->command->line('Duplicate tasks'); - $this->command->line('---------------'); - $this->command->line('These tasks could not be monitored because they have a duplicate name.'); - $this->command->line(''); - - $headers = ['Type', 'Frequency']; - $rows = $duplicateTasks->map(function (Task $task) { - return [ - 'name' => $task->name(), - 'type' => ucfirst($task->type()), - 'cron_expression' => $task->humanReadableCron(), - ]; - }); - - $this->command->table($headers, $rows); - - $this->command->line(''); - $this->command->line('To monitor these tasks you should add `->monitorName()` in the schedule to manually specify a unique name.'); - } -} diff --git a/src/Commands/Tables/MonitoredTasksTable.php b/src/Commands/Tables/MonitoredTasksTable.php deleted file mode 100644 index 8844823..0000000 --- a/src/Commands/Tables/MonitoredTasksTable.php +++ /dev/null @@ -1,125 +0,0 @@ -command->line(''); - $this->command->line('Monitored tasks'); - $this->command->line('---------------'); - - $tasks = ScheduledTasks::createForSchedule() - ->uniqueTasks() - ->filter(fn (Task $task) => $task->isBeingMonitored()); - - if ($tasks->isEmpty()) { - $this->command->line(''); - $this->command->warn('There currently are no tasks being monitored!'); - - return; - } - - $headers = [ - 'Name', - 'Type', - 'Frequency', - 'Last started at', - 'Last finished at', - 'Last failed at', - 'Next run date', - 'Grace time', - ]; - - if ($this->usingOhDear()) { - $headers = array_merge($headers, [ - 'Registered at Oh Dear', - ]); - } - - $dateFormat = config('schedule-monitor.date_format'); - - $rows = $tasks->map(function (Task $task) use ($dateFormat) { - $row = [ - 'name' => $task->name(), - 'type' => ucfirst($task->type()), - 'cron_expression' => $task->humanReadableCron(), - 'started_at' => optional($task->lastRunStartedAt())->format($dateFormat) ?? 'Did not start yet', - 'finished_at' => $this->getLastRunFinishedAt($task), - 'failed_at' => $this->getLastRunFailedAt($task), - 'next_run' => $task->nextRunAt()->format($dateFormat), - 'grace_time' => $task->graceTimeInMinutes(), - ]; - - if ($this->usingOhDear()) { - $row = array_merge($row, [ - 'registered_at_oh_dear' => $task->isBeingMonitoredAtOhDear() ? '✅' : '❌', - ]); - } - - return $row; - }); - - $this->command->table($headers, $rows); - - if ($this->usingOhDear()) { - if ($tasks->contains(fn (Task $task) => ! $task->isBeingMonitoredAtOhDear())) { - $this->command->line(''); - $this->command->line('Some tasks are not registered on Oh Dear. You will not be notified when they do not run on time.'); - $this->command->line('Run `php artisan schedule-monitor:sync` to register them and receive notifications.'); - } - } - } - - public function getLastRunFinishedAt(Task $task): string - { - $dateFormat = config('schedule-monitor.date_format'); - - $formattedLastRunFinishedAt = optional($task->lastRunFinishedAt())->format($dateFormat) ?? ''; - - if ($task->lastRunFinishedTooLate()) { - $formattedLastRunFinishedAt = "{$formattedLastRunFinishedAt}"; - } - - return $formattedLastRunFinishedAt; - } - - public function getLastRunFailedAt(Task $task): string - { - if (! $lastRunFailedAt = $task->lastRunFailedAt()) { - return ''; - } - - $dateFormat = config('schedule-monitor.date_format'); - - $formattedLastFailedAt = $lastRunFailedAt->format($dateFormat); - - if ($task->lastRunFailed()) { - $formattedLastFailedAt = "{$formattedLastFailedAt}"; - } - - return $formattedLastFailedAt; - } - - protected function usingOhDear(): bool - { - if (! class_exists(OhDear::class)) { - return false; - } - - if (empty(config('schedule-monitor.oh_dear.api_token'))) { - return false; - } - - if (empty(config('schedule-monitor.oh_dear.site_id'))) { - return false; - } - - return true; - } -} diff --git a/src/Commands/Tables/ReadyForMonitoringTasksTable.php b/src/Commands/Tables/ReadyForMonitoringTasksTable.php deleted file mode 100644 index 92e18f2..0000000 --- a/src/Commands/Tables/ReadyForMonitoringTasksTable.php +++ /dev/null @@ -1,41 +0,0 @@ -uniqueTasks() - ->reject(fn (Task $task) => $task->isBeingMonitored()); - - if ($tasks->isEmpty()) { - return; - } - - $this->command->line(''); - $this->command->line('Run sync to start monitoring'); - $this->command->line('----------------------------'); - $this->command->line(''); - $this->command->line('These tasks will be monitored after running `php artisan schedule-monitor:sync`'); - $this->command->line(''); - - $tasks = ScheduledTasks::createForSchedule() - ->uniqueTasks() - ->reject(fn (Task $task) => $task->isBeingMonitored()); - - $headers = ['Name', 'Type', 'Frequency']; - $rows = $tasks->map(function (Task $task) { - return [ - 'name' => $task->name(), - 'type' => ucfirst($task->type()), - 'cron_expression' => $task->humanReadableCron(), - ]; - }); - $this->command->table($headers, $rows); - } -} diff --git a/src/Commands/Tables/ScheduledTasksTable.php b/src/Commands/Tables/ScheduledTasksTable.php deleted file mode 100644 index 2bc26ff..0000000 --- a/src/Commands/Tables/ScheduledTasksTable.php +++ /dev/null @@ -1,17 +0,0 @@ -command = $command; - } - - abstract public function render(): void; -} diff --git a/src/Commands/Tables/UnnamedTasksTable.php b/src/Commands/Tables/UnnamedTasksTable.php deleted file mode 100644 index f4052ed..0000000 --- a/src/Commands/Tables/UnnamedTasksTable.php +++ /dev/null @@ -1,38 +0,0 @@ -unnamedTasks(); - - if ($unnamedTasks->isEmpty()) { - return; - } - - $this->command->line(''); - $this->command->line('Unnamed tasks'); - $this->command->line('-------------'); - $this->command->line('These tasks cannot be monitored because no name could be determined for them.'); - $this->command->line(''); - - - $headers = ['Type', 'Frequency']; - $rows = $unnamedTasks->map(function (Task $task) { - return [ - 'type' => ucfirst($task->type()), - 'cron_expression' => $task->humanReadableCron(), - ]; - }); - - $this->command->table($headers, $rows); - - $this->command->line(''); - $this->command->line('To monitor these tasks you should add `->monitorName()` in the schedule to manually specify a name.'); - } -} diff --git a/src/Commands/VerifyCommand.php b/src/Commands/VerifyCommand.php index 4799027..dcaf97f 100644 --- a/src/Commands/VerifyCommand.php +++ b/src/Commands/VerifyCommand.php @@ -5,6 +5,7 @@ use Exception; use Illuminate\Console\Command; use OhDear\PhpSdk\OhDear; +use function Termwind\render; class VerifyCommand extends Command { @@ -16,8 +17,9 @@ public function handle() { $ohDearConfig = config('schedule-monitor.oh_dear'); - $this->info('Verifying if Oh Dear is configured correctly...'); - $this->line(''); + render(view('schedule-monitor::alert', [ + 'message' => 'Verifying if Oh Dear is configured correctly...', + ])); $this ->verifySdkInstalled() @@ -25,9 +27,12 @@ public function handle() ->verifySiteId($ohDearConfig) ->verifyConnection($ohDearConfig); - $this->line(''); - $this->info('All ok!'); - $this->info('Run `php artisan schedule-monitor:sync` to sync your scheduled tasks with Oh Dear.'); + render(view('schedule-monitor::alert', [ + 'message' => <<All ok! Run php artisan schedule-monitor:sync + to sync your scheduled tasks with oh dear. + HTML, + ])); } public function verifySdkInstalled(): self @@ -36,7 +41,9 @@ public function verifySdkInstalled(): self throw new Exception("You must install the Oh Dear SDK in order to sync your schedule with Oh Dear. Run `composer require ohdearapp/ohdear-php-sdk`."); } - $this->comment('The Oh Dear SDK is installed.'); + render(view('schedule-monitor::alert', [ + 'message' => 'The Oh Dear SDK is installed.', + ])); return $this; } @@ -47,7 +54,9 @@ protected function verifyApiToken(array $ohDearConfig): self throw new Exception('No API token found. Make sure you added an API token to the `api_token` key of the `schedule-monitor` config file. You can generate a new token here: https://ohdear.app/user/api-tokens'); } - $this->comment('Oh Dear API token found.'); + render(view('schedule-monitor::alert', [ + 'message' => 'Oh Dear API token found.', + ])); return $this; } @@ -58,7 +67,9 @@ protected function verifySiteId(array $ohDearConfig): self throw new Exception('No site id found. Make sure you added an site id to the `site_id` key of the `schedule-monitor` config file. You can found your site id on the settings page of a site on Oh Dear.'); } - $this->comment('Oh Dear site id found.'); + render(view('schedule-monitor::alert', [ + 'message' => 'Oh Dear site id found.', + ])); return $this; } @@ -69,7 +80,9 @@ protected function verifyConnection(array $ohDearConfig) $site = app(OhDear::class)->site($ohDearConfig['site_id']); - $this->comment("Successfully connected to Oh Dear. The configured site URL is: {$site->sortUrl}"); + render(view('schedule-monitor::alert', [ + 'message' => "Successfully connected to Oh Dear. The configured site URL is: {$site->sortUrl}", + ])); return $this; } diff --git a/src/ScheduleMonitorServiceProvider.php b/src/ScheduleMonitorServiceProvider.php index 11632b3..f5f816c 100644 --- a/src/ScheduleMonitorServiceProvider.php +++ b/src/ScheduleMonitorServiceProvider.php @@ -23,6 +23,7 @@ public function configurePackage(Package $package): void { $package ->name('laravel-schedule-monitor') + ->hasViews() ->hasConfigFile() ->hasMigrations('create_schedule_monitor_tables') ->hasCommands([ diff --git a/src/Support/ScheduledTasks/ScheduledTasks.php b/src/Support/ScheduledTasks/ScheduledTasks.php index de6bd9a..24bd497 100644 --- a/src/Support/ScheduledTasks/ScheduledTasks.php +++ b/src/Support/ScheduledTasks/ScheduledTasks.php @@ -55,6 +55,18 @@ public function duplicateTasks(): Collection ->values(); } + public function readyForMonitoringTasks(): Collection + { + return $this->uniqueTasks() + ->reject(fn (Task $task) => $task->isBeingMonitored()); + } + + public function monitoredTasks(): Collection + { + return $this->uniqueTasks() + ->filter(fn (Task $task) => $task->isBeingMonitored()); + } + public function unmonitoredTasks(): Collection { return $this->tasks->reject(fn (Task $task) => $task->shouldMonitor()); diff --git a/tests/TestCase.php b/tests/TestCase.php index 37a96c9..f3c5fd6 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -10,6 +10,8 @@ use Spatie\ScheduleMonitor\ScheduleMonitorServiceProvider; use Spatie\ScheduleMonitor\Tests\TestClasses\FakeOhDear; use Spatie\ScheduleMonitor\Tests\TestClasses\TestKernel; +use Symfony\Component\Console\Output\BufferedOutput; +use function Termwind\renderUsing; class TestCase extends Orchestra { @@ -38,6 +40,8 @@ protected function setUp(): void Factory::guessFactoryNamesUsing( fn (string $modelName) => 'Spatie\\ScheduleMonitor\\Database\\Factories\\' . class_basename($modelName) . 'Factory' ); + + renderUsing(new BufferedOutput()); } protected function getPackageProviders($app)