From 8d6e8b22dc23080be1dc9754373995e67712907e Mon Sep 17 00:00:00 2001 From: Marick van Tuil Date: Sat, 23 Jul 2022 17:50:07 +0200 Subject: [PATCH 01/18] Add initial draft of job releasing --- CHANGELOG.md | 6 ++++++ README.md | 8 +++++++- src/CloudTasksApiFake.php | 5 +++++ src/CloudTasksJob.php | 7 +++++++ src/CloudTasksQueue.php | 15 +++++++++++++- src/TaskHandler.php | 14 ++++++++----- tests/TaskHandlerTest.php | 42 +++++++++++++++++++++++++++++---------- 7 files changed, 80 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc90cd7..b3c729f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## 3.?.0 - ????-??-?? + +**Added** + +- Jobs can now be released back onto the queue. + ## 3.2.1 - 2022-09-02 **Fixed** diff --git a/README.md b/README.md index 5fa0a04..ee2f96e 100644 --- a/README.md +++ b/README.md @@ -79,13 +79,19 @@ Please check the table below on what the values mean and what their value should
- How it works + How it works & Differences
Using Cloud Tasks as a Laravel queue driver is fundamentally different than other Laravel queue drivers, like Redis. Typically a Laravel queue has a worker that listens to incoming jobs using the `queue:work` / `queue:listen` command. With Cloud Tasks, this is not the case. Instead, Cloud Tasks will schedule the job for you and make an HTTP request to your application with the job payload. There is no need to run a `queue:work/listen` command. + +#### Good to know + +- The "Min backoff" and "Max backoff" options in Cloud Tasks are ignored. This is intentional: Laravel has its own backoff feature (which is more powerful than what Cloud Tasks offers) and therefore I have chosen that over the Cloud Tasks one. +- Similarly to the backoff feature, I have also chosen to let the package do job retries the 'Laravel way'. In Cloud Tasks, when a task throws an exception, Cloud Tasks will decide for itself when to retry the task (based on the backoff values). It will also manage its own state and knows how many times a task has been retried. This is different from Laravel. In typical Laravel queues, when a job throws an exception, the job is deleted and released back onto the queue. In order to support Laravel's backoff feature, this package must behave the same way about job retries. +
Dashboard (beta) diff --git a/src/CloudTasksApiFake.php b/src/CloudTasksApiFake.php index da3b56c..59a046a 100644 --- a/src/CloudTasksApiFake.php +++ b/src/CloudTasksApiFake.php @@ -91,4 +91,9 @@ public function assertTaskCreated(Closure $closure): void Assert::assertTrue($count > 0, 'Task was not created.'); } + + public function assertCreatedTaskCount(int $count): void + { + Assert::assertCount($count, $this->createdTasks); + } } diff --git a/src/CloudTasksJob.php b/src/CloudTasksJob.php index 63f1e12..f99c3a1 100644 --- a/src/CloudTasksJob.php +++ b/src/CloudTasksJob.php @@ -95,4 +95,11 @@ public function delete(): void $this->cloudTasksQueue->delete($this); } + + public function release($delay = 0) + { + parent::release(); + + $this->cloudTasksQueue->release($this, $delay); + } } diff --git a/src/CloudTasksQueue.php b/src/CloudTasksQueue.php index 7fde5e9..5425884 100644 --- a/src/CloudTasksQueue.php +++ b/src/CloudTasksQueue.php @@ -93,7 +93,9 @@ function ($payload, $queue) { */ public function pushRaw($payload, $queue = null, array $options = []) { - return $this->pushToCloudTasks($queue, $payload); + $delay = ! empty($options['delay']) ? $options['delay'] : 0; + + $this->pushToCloudTasks($queue, $payload, $delay); } /** @@ -217,6 +219,17 @@ public function delete(CloudTasksJob $job): void CloudTasksApi::deleteTask($taskName); } + public function release(CloudTasksJob $job, int $delay = 0): void + { + $job->delete(); + + $payload = $job->getRawBody(); + + $options = ['delay' => $delay]; + + $this->pushRaw($payload, $job->getQueue(), $options); + } + private function createTask(): Task { return app(Task::class); diff --git a/src/TaskHandler.php b/src/TaskHandler.php index 90c725b..3ecf69c 100644 --- a/src/TaskHandler.php +++ b/src/TaskHandler.php @@ -7,6 +7,7 @@ use Illuminate\Bus\Queueable; use Illuminate\Contracts\Encryption\Encrypter; use Illuminate\Queue\Jobs\Job; +use Illuminate\Queue\QueueManager; use Illuminate\Queue\WorkerOptions; use Illuminate\Support\Str; use Illuminate\Validation\ValidationException; @@ -102,11 +103,14 @@ private function loadQueueConnectionConfiguration(array $task): void * @var stdClass $command */ $command = self::getCommandProperties($task['data']['command']); - $connection = $command['connection'] ?? config('queue.default'); - $this->config = array_merge( - (array) config("queue.connections.{$connection}"), - ['connection' => $connection] - ); + $connection = $command->connection ?? config('queue.default'); + $baseConfig = config('queue.connections.' . $connection); + $config = (new CloudTasksConnector())->connect($baseConfig)->config; + + // The connection name from the config may not be the actual connection name + $config['connection'] = $connection; + + $this->config = $config; } private function setQueue(): void diff --git a/tests/TaskHandlerTest.php b/tests/TaskHandlerTest.php index 1869a90..873e9be 100644 --- a/tests/TaskHandlerTest.php +++ b/tests/TaskHandlerTest.php @@ -268,17 +268,17 @@ public function after_max_attempts_it_will_delete_the_task() // Act & Assert $job->run(); - CloudTasksApi::assertDeletedTaskCount(0); - CloudTasksApi::assertTaskNotDeleted($job->task->getName()); + CloudTasksApi::assertDeletedTaskCount(1); + CloudTasksApi::assertTaskDeleted($job->task->getName()); $this->assertDatabaseCount('failed_jobs', 0); $job->run(); - CloudTasksApi::assertDeletedTaskCount(0); - CloudTasksApi::assertTaskNotDeleted($job->task->getName()); + CloudTasksApi::assertDeletedTaskCount(2); + CloudTasksApi::assertTaskDeleted($job->task->getName()); $this->assertDatabaseCount('failed_jobs', 0); $job->run(); - CloudTasksApi::assertDeletedTaskCount(1); + CloudTasksApi::assertDeletedTaskCount(3); CloudTasksApi::assertTaskDeleted($job->task->getName()); $this->assertDatabaseCount('failed_jobs', 1); } @@ -300,8 +300,8 @@ public function after_max_retry_until_it_will_log_to_failed_table_and_delete_the $job->run(); // Assert - CloudTasksApi::assertDeletedTaskCount(0); - CloudTasksApi::assertTaskNotDeleted($job->task->getName()); + CloudTasksApi::assertDeletedTaskCount(1); + CloudTasksApi::assertTaskDeleted($job->task->getName()); $this->assertDatabaseCount('failed_jobs', 0); // Act @@ -309,7 +309,7 @@ public function after_max_retry_until_it_will_log_to_failed_table_and_delete_the $job->run(); // Assert - CloudTasksApi::assertDeletedTaskCount(1); + CloudTasksApi::assertDeletedTaskCount(2); CloudTasksApi::assertTaskDeleted($job->task->getName()); $this->assertDatabaseCount('failed_jobs', 1); } @@ -330,8 +330,8 @@ public function test_unlimited_max_attempts() $job = $this->dispatch(new FailingJob()); foreach (range(1, 50) as $attempt) { $job->run(); - CloudTasksApi::assertDeletedTaskCount(0); - CloudTasksApi::assertTaskNotDeleted($job->task->getName()); + CloudTasksApi::assertDeletedTaskCount($attempt); + CloudTasksApi::assertTaskDeleted($job->task->getName()); $this->assertDatabaseCount('failed_jobs', 0); } } @@ -408,4 +408,26 @@ public function it_can_handle_encrypted_jobs() Log::assertLogged('EncryptedJob:success'); } + + public function failing_jobs_are_released() + { + // Arrange + OpenIdVerificator::fake(); + CloudTasksApi::partialMock()->shouldReceive('getRetryConfig')->andReturn( + (new RetryConfig())->setMaxAttempts(3) + ); + + // Act + $job = $this->dispatch(new FailingJob()); + + CloudTasksApi::assertDeletedTaskCount(0); + CloudTasksApi::assertCreatedTaskCount(1); + CloudTasksApi::assertTaskNotDeleted($job->task->getName()); + + $job->run(); + + CloudTasksApi::assertDeletedTaskCount(1); + CloudTasksApi::assertCreatedTaskCount(2); + CloudTasksApi::assertTaskDeleted($job->task->getName()); + } } From 8f41ea19d50d68cd39fab55d719f4aaf7dc24503 Mon Sep 17 00:00:00 2001 From: Marick van Tuil Date: Sun, 24 Jul 2022 11:31:49 +0200 Subject: [PATCH 02/18] Track job attempts internally --- src/CloudTasksQueue.php | 22 ++++++++++++++++++++++ src/TaskHandler.php | 2 +- tests/TestCase.php | 13 +++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/CloudTasksQueue.php b/src/CloudTasksQueue.php index 5425884..6b3b5ac 100644 --- a/src/CloudTasksQueue.php +++ b/src/CloudTasksQueue.php @@ -143,11 +143,19 @@ protected function pushToCloudTasks($queue, $payload, $delay = 0) // we will add it manually here if it's not present yet. [$payload, $uuid] = $this->withUuid($payload); + // Since 3.x tasks are released back onto the queue after an exception has + // been thrown. This means we lose the native [X-CloudTasks-TaskRetryCount] header + // value and need to manually set and update the number of times a task has been attempted. + $payload = $this->withAttempts($payload); + $httpRequest->setBody($payload); $task = $this->createTask(); $task->setHttpRequest($httpRequest); + // The deadline for requests sent to the app. If the app does not respond by + // this deadline then the request is cancelled and the attempt is marked as + // a failure. Cloud Tasks will retry the task according to the RetryConfig. if (!empty($this->config['dispatch_deadline'])) { $task->setDispatchDeadline(new Duration(['seconds' => $this->config['dispatch_deadline']])); } @@ -182,6 +190,20 @@ private function withUuid(string $payload): array ]; } + private function withAttempts(string $payload): string + { + /** @var array $decoded */ + $decoded = json_decode($payload, true); + + if (!isset($decoded['internal']['attempts'])) { + $decoded['internal']['attempts'] = 0; + } else { + $decoded['internal']['attempts']++; + } + + return json_encode($decoded); + } + /** * Pop the next job off of the queue. * diff --git a/src/TaskHandler.php b/src/TaskHandler.php index 3ecf69c..c1b39e0 100644 --- a/src/TaskHandler.php +++ b/src/TaskHandler.php @@ -124,7 +124,7 @@ private function handleTask(array $task): void $this->loadQueueRetryConfig($job); - $job->setAttempts((int) request()->header('X-CloudTasks-TaskRetryCount')); + $job->setAttempts($task['internal']['attempts']); $job->setMaxTries($this->retryConfig->getMaxAttempts()); // If the job is being attempted again we also check if a diff --git a/tests/TestCase.php b/tests/TestCase.php index f49712a..96debaf 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -149,6 +149,8 @@ public function run(): void rescue(function (): void { app(TaskHandler::class)->handle($this->payload); }); + + $this->payload = $this->incrementAttempts($this->payload); } public function runWithoutExceptionHandler(): void @@ -158,6 +160,17 @@ public function runWithoutExceptionHandler(): void app(TaskHandler::class)->handle($this->payload); + $this->payload = $this->incrementAttempts($this->payload); + } + + private function incrementAttempts(string $payload): string + { + $decoded = \Safe\json_decode($payload, true); + + $decoded['internal']['attempts'] ??= 0; + $decoded['internal']['attempts']++; + + return json_encode($decoded); } }; } From eecf3f871dafc5fb503e8905b5901b129f37d9e1 Mon Sep 17 00:00:00 2001 From: Marick van Tuil Date: Fri, 2 Sep 2022 21:46:46 +0200 Subject: [PATCH 03/18] Prevent monitoring dashboard from creating multiple tasks --- src/DashboardService.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/DashboardService.php b/src/DashboardService.php index 49e34d2..cb33ffd 100644 --- a/src/DashboardService.php +++ b/src/DashboardService.php @@ -31,6 +31,12 @@ private function getTaskBody(Task $task): string public function add(string $queue, Task $task): void { + $uuid = $this->getTaskUuid($task); + + if (StackkitCloudTask::whereTaskUuid($uuid)->exists()) { + return; + } + $metadata = new TaskMetadata(); $metadata->payload = $this->getTaskBody($task); @@ -51,7 +57,7 @@ public function add(string $queue, Task $task): void DB::table('stackkit_cloud_tasks') ->insert([ - 'task_uuid' => $this->getTaskUuid($task), + 'task_uuid' => $uuid, 'name' => $this->getTaskName($task), 'queue' => $queue, 'payload' => $this->getTaskBody($task), From 6932b21bd5bd144df2638dcbea993d9154676dbb Mon Sep 17 00:00:00 2001 From: Marick van Tuil Date: Fri, 2 Sep 2022 21:50:45 +0200 Subject: [PATCH 04/18] Remove X-CloudTasks-TaskRetryCount usages --- src/TaskHandler.php | 2 -- tests/TaskHandlerTest.php | 3 --- tests/TestCase.php | 6 ------ 3 files changed, 11 deletions(-) diff --git a/src/TaskHandler.php b/src/TaskHandler.php index c1b39e0..41cf9a4 100644 --- a/src/TaskHandler.php +++ b/src/TaskHandler.php @@ -75,13 +75,11 @@ private function captureTask($task): array 'json' => $task, 'task' => $array, 'name_header' => request()->header('X-CloudTasks-Taskname'), - 'retry_count_header' => request()->header('X-CloudTasks-TaskRetryCount'), ], [ 'json' => 'required|json', 'task' => 'required|array', 'task.data' => 'required|array', 'name_header' => 'required|string', - 'retry_count_header' => 'required|numeric', ]); try { diff --git a/tests/TaskHandlerTest.php b/tests/TaskHandlerTest.php index 873e9be..9cbd9e1 100644 --- a/tests/TaskHandlerTest.php +++ b/tests/TaskHandlerTest.php @@ -129,17 +129,14 @@ public function it_validates_headers(bool $withHeaders) $withHeaders ? [ 'X-CloudTasks-Taskname' => 'MyTask', - 'X-CloudTasks-TaskRetryCount' => 0, ] : [] ); // Assert if ($withHeaders) { $response->assertJsonMissingValidationErrors('name_header'); - $response->assertJsonMissingValidationErrors('retry_count_header'); } else { $response->assertJsonValidationErrors('name_header'); - $response->assertJsonValidationErrors('retry_count_header'); } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 96debaf..b444163 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -143,9 +143,6 @@ public function __construct(string $payload, array $payloadAsArray, Task $task) public function run(): void { - $taskRetryCount = request()->header('X-CloudTasks-TaskRetryCount', -1); - request()->headers->set('X-CloudTasks-TaskRetryCount', $taskRetryCount + 1); - rescue(function (): void { app(TaskHandler::class)->handle($this->payload); }); @@ -155,9 +152,6 @@ public function run(): void public function runWithoutExceptionHandler(): void { - $taskRetryCount = request()->header('X-CloudTasks-TaskRetryCount', -1); - request()->headers->set('X-CloudTasks-TaskRetryCount', $taskRetryCount + 1); - app(TaskHandler::class)->handle($this->payload); $this->payload = $this->incrementAttempts($this->payload); From 251b9d834281215cd082ded4e156bbbf08e5cf14 Mon Sep 17 00:00:00 2001 From: Marick van Tuil Date: Sat, 10 Sep 2022 23:10:06 +0200 Subject: [PATCH 05/18] Update tests that fail jobs multiple times --- src/CloudTasksJob.php | 16 ++++++-- tests/CloudTasksDashboardTest.php | 9 ++--- tests/TaskHandlerTest.php | 63 ++++++++++++++++++++++++------- tests/TestCase.php | 50 ++++++++++++++---------- 4 files changed, 97 insertions(+), 41 deletions(-) diff --git a/src/CloudTasksJob.php b/src/CloudTasksJob.php index f99c3a1..ddeea9d 100644 --- a/src/CloudTasksJob.php +++ b/src/CloudTasksJob.php @@ -9,8 +9,13 @@ class CloudTasksJob extends LaravelJob implements JobContract { + /** + * The Cloud Tasks raw job payload (request payload). + * + * @var array + */ private array $job; - private ?int $attempts; + private ?int $maxTries; public ?int $retryUntil = null; @@ -29,6 +34,11 @@ public function __construct(array $job, CloudTasksQueue $cloudTasksQueue) $this->queue = $command['queue'] ?? config('queue.connections.' .config('queue.default') . '.queue'); } + public function job() + { + return $this->job; + } + public function getJobId(): string { return $this->job['uuid']; @@ -46,12 +56,12 @@ public function getRawBody(): string public function attempts(): ?int { - return $this->attempts; + return $this->job['internal']['attempts']; } public function setAttempts(int $attempts): void { - $this->attempts = $attempts; + $this->job['internal']['attempts'] = $attempts; } public function setMaxTries(int $maxTries): void diff --git a/tests/CloudTasksDashboardTest.php b/tests/CloudTasksDashboardTest.php index aefcc9e..373267e 100644 --- a/tests/CloudTasksDashboardTest.php +++ b/tests/CloudTasksDashboardTest.php @@ -258,8 +258,7 @@ public function when_a_job_is_dispatched_it_will_be_added_to_the_dashboard() 'status' => 'queued', 'name' => SimpleJob::class, ]); - $payload = \Safe\json_decode($task->getMetadata()['payload'], true); - $this->assertSame($payload, $job->payloadAsArray); + $this->assertSame($task->getMetadata()['payload'], $job->payload); } /** @@ -396,9 +395,9 @@ public function when_a_job_fails_it_will_be_updated_in_the_dashboard() ); $job = $this->dispatch(new FailingJob()); - $job->run(); - $job->run(); - $job->run(); + $releasedJob = $job->runAndGetReleasedJob(); + $releasedJob = $releasedJob->runAndGetReleasedJob(); + $releasedJob->run(); // Assert $task = StackkitCloudTask::firstOrFail(); diff --git a/tests/TaskHandlerTest.php b/tests/TaskHandlerTest.php index 9cbd9e1..1a0b9eb 100644 --- a/tests/TaskHandlerTest.php +++ b/tests/TaskHandlerTest.php @@ -7,6 +7,7 @@ use Google\Protobuf\Duration; use Illuminate\Queue\Events\JobProcessed; use Illuminate\Queue\Events\JobProcessing; +use Illuminate\Queue\Events\JobReleasedAfterException; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Log; use Illuminate\Validation\ValidationException; @@ -239,13 +240,13 @@ public function after_max_attempts_it_will_log_to_failed_table() // Act & Assert $this->assertDatabaseCount('failed_jobs', 0); - $job->run(); + $releasedJob = $job->runAndGetReleasedJob(); $this->assertDatabaseCount('failed_jobs', 0); - $job->run(); + $releasedJob = $releasedJob->runAndGetReleasedJob(); $this->assertDatabaseCount('failed_jobs', 0); - $job->run(); + $releasedJob->run(); $this->assertDatabaseCount('failed_jobs', 1); } @@ -264,17 +265,17 @@ public function after_max_attempts_it_will_delete_the_task() $job = $this->dispatch(new FailingJob()); // Act & Assert - $job->run(); + $releasedJob = $job->runAndGetReleasedJob(); CloudTasksApi::assertDeletedTaskCount(1); CloudTasksApi::assertTaskDeleted($job->task->getName()); $this->assertDatabaseCount('failed_jobs', 0); - $job->run(); + $releasedJob = $releasedJob->runAndGetReleasedJob(); CloudTasksApi::assertDeletedTaskCount(2); CloudTasksApi::assertTaskDeleted($job->task->getName()); $this->assertDatabaseCount('failed_jobs', 0); - $job->run(); + $releasedJob->run(); CloudTasksApi::assertDeletedTaskCount(3); CloudTasksApi::assertTaskDeleted($job->task->getName()); $this->assertDatabaseCount('failed_jobs', 1); @@ -294,7 +295,7 @@ public function after_max_retry_until_it_will_log_to_failed_table_and_delete_the $job = $this->dispatch(new FailingJob()); // Act - $job->run(); + $releasedJob = $job->runAndGetReleasedJob(); // Assert CloudTasksApi::assertDeletedTaskCount(1); @@ -303,7 +304,7 @@ public function after_max_retry_until_it_will_log_to_failed_table_and_delete_the // Act CloudTasksApi::partialMock()->shouldReceive('getRetryUntilTimestamp')->andReturn(1); - $job->run(); + $releasedJob->run(); // Assert CloudTasksApi::assertDeletedTaskCount(2); @@ -353,15 +354,15 @@ public function test_max_attempts_in_combination_with_retry_until() $job = $this->dispatch(new FailingJob()); // Act & Assert - $job->run(); - $job->run(); + $releasedJob = $job->runAndGetReleasedJob(); + $releasedJob = $releasedJob->runAndGetReleasedJob(); # After 2 attempts both Laravel versions should report the same: 2 errors and 0 failures. - $task = StackkitCloudTask::whereTaskUuid($job->payloadAsArray['uuid'])->firstOrFail(); + $task = StackkitCloudTask::whereTaskUuid($job->payloadAsArray('uuid'))->firstOrFail(); $this->assertEquals(2, $task->getNumberOfAttempts()); $this->assertEquals('error', $task->status); - $job->run(); + $releasedJob->run(); # Max attempts was reached # Laravel 5, 6, 7: fail because max attempts was reached @@ -375,7 +376,7 @@ public function test_max_attempts_in_combination_with_retry_until() } CloudTasksApi::shouldReceive('getRetryUntilTimestamp')->andReturn(time() - 1); - $job->run(); + $releasedJob->run(); $this->assertEquals('failed', $task->fresh()->status); } @@ -400,12 +401,15 @@ public function it_can_handle_encrypted_jobs() // Assert $this->assertStringContainsString( 'O:26:"Tests\Support\EncryptedJob"', - decrypt($job->payloadAsArray['data']['command']), + decrypt($job->payloadAsArray('data.command')), ); Log::assertLogged('EncryptedJob:success'); } + /** + * @test + */ public function failing_jobs_are_released() { // Arrange @@ -413,6 +417,7 @@ public function failing_jobs_are_released() CloudTasksApi::partialMock()->shouldReceive('getRetryConfig')->andReturn( (new RetryConfig())->setMaxAttempts(3) ); + Event::fake([JobReleasedAfterException::class]); // Act $job = $this->dispatch(new FailingJob()); @@ -426,5 +431,35 @@ public function failing_jobs_are_released() CloudTasksApi::assertDeletedTaskCount(1); CloudTasksApi::assertCreatedTaskCount(2); CloudTasksApi::assertTaskDeleted($job->task->getName()); + Event::assertDispatched(JobReleasedAfterException::class, function ($event) { + return $event->job->attempts() === 1; + }); + } + + /** + * @test + */ + public function attempts_are_tracked_internally() + { + // Arrange + CloudTasksApi::fake(); + OpenIdVerificator::fake(); + Event::fake([JobReleasedAfterException::class]); + + // Act & Assert + $job = $this->dispatch(new FailingJob()); + $job->run(); + $releasedJob = null; + + Event::assertDispatched(JobReleasedAfterException::class, function ($event) use (&$releasedJob) { + $releasedJob = $event->job->getRawBody(); + return $event->job->attempts() === 1; + }); + + $this->runFromPayload($releasedJob); + + Event::assertDispatched(JobReleasedAfterException::class, function ($event) { + return $event->job->attempts() === 2; + }); } } diff --git a/tests/TestCase.php b/tests/TestCase.php index b444163..b7b3a61 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -9,10 +9,12 @@ use Google\Cloud\Tasks\V2\RetryConfig; use Google\Cloud\Tasks\V2\Task; use Illuminate\Foundation\Testing\DatabaseTransactions; +use Illuminate\Queue\Events\JobReleasedAfterException; use Illuminate\Support\Facades\DB; use Google\Cloud\Tasks\V2\CloudTasksClient; use Illuminate\Support\Facades\Event; use Mockery; +use Stackkit\LaravelGoogleCloudTasksQueue\CloudTasksJob; use Stackkit\LaravelGoogleCloudTasksQueue\TaskCreated; use Stackkit\LaravelGoogleCloudTasksQueue\TaskHandler; @@ -25,6 +27,8 @@ class TestCase extends \Orchestra\Testbench\TestCase */ public $client; + public string $releasedJobPayload; + protected function setUp(): void { parent::setUp(); @@ -32,6 +36,13 @@ protected function setUp(): void $this->withFactories(__DIR__ . '/../factories'); $this->defaultHeaders['Authorization'] = 'Bearer ' . encrypt(time() + 10); + + Event::listen( + JobReleasedAfterException::class, + function (JobReleasedAfterException $event) { + $this->releasedJobPayload = $event->job->getRawBody(); + } + ); } /** @@ -129,16 +140,16 @@ public function dispatch($job) dispatch($job); - return new class($payload, $payloadAsArray, $task) { + return new class($payload, $task, $this) { public string $payload; - public array $payloadAsArray; public Task $task; + public TestCase $testCase; - public function __construct(string $payload, array $payloadAsArray, Task $task) + public function __construct(string $payload, Task $task, TestCase $testCase) { $this->payload = $payload; - $this->payloadAsArray = $payloadAsArray; $this->task = $task; + $this->testCase = $testCase; } public function run(): void @@ -146,41 +157,42 @@ public function run(): void rescue(function (): void { app(TaskHandler::class)->handle($this->payload); }); - - $this->payload = $this->incrementAttempts($this->payload); } public function runWithoutExceptionHandler(): void { app(TaskHandler::class)->handle($this->payload); - - $this->payload = $this->incrementAttempts($this->payload); } - private function incrementAttempts(string $payload): string + public function runAndGetReleasedJob(): self { - $decoded = \Safe\json_decode($payload, true); + rescue(function (): void { + app(TaskHandler::class)->handle($this->payload); + }); + + return new self( + $this->testCase->releasedJobPayload, + $this->task, + $this->testCase + ); + } - $decoded['internal']['attempts'] ??= 0; - $decoded['internal']['attempts']++; + public function payloadAsArray(string $key = '') + { + $decoded = json_decode($this->payload, true); - return json_encode($decoded); + return data_get($decoded, $key ?: null); } }; } - public function runFromPayload(array $payload): void + public function runFromPayload(string $payload): void { rescue(function () use ($payload) { app(TaskHandler::class)->handle($payload); }); } - public function dispatchAndRun($job): void - { - $this->runFromPayload($this->dispatch($job)); - } - public function assertTaskDeleted(string $taskId): void { try { From a5f58438e1fec3e9d8be3712abf762edd1b3aa79 Mon Sep 17 00:00:00 2001 From: Marick van Tuil Date: Sun, 11 Sep 2022 12:23:14 +0200 Subject: [PATCH 06/18] Add fallback JobReleasedAfterException event --- composer.json | 18 +++++++++++++++ src/CloudTasksJob.php | 12 ++++++++++ src/JobReleasedAfterException.php | 37 +++++++++++++++++++++++++++++++ tests/QueueTest.php | 8 +++++-- tests/TaskHandlerTest.php | 13 +++++------ tests/TestCase.php | 19 +++++++++++----- 6 files changed, 91 insertions(+), 16 deletions(-) create mode 100644 src/JobReleasedAfterException.php diff --git a/composer.json b/composer.json index c01c313..76ee4cc 100644 --- a/composer.json +++ b/composer.json @@ -36,5 +36,23 @@ "Stackkit\\LaravelGoogleCloudTasksQueue\\CloudTasksServiceProvider" ] } + }, + "scripts": { + "l9": [ + "composer require laravel/framework:9.* orchestra/testbench:7.* --no-interaction --no-update", + "composer update --prefer-stable --prefer-dist --no-interaction --no-suggest" + ], + "l8": [ + "composer require laravel/framework:8.* orchestra/testbench:6.* --no-interaction --no-update", + "composer update --prefer-stable --prefer-dist --no-interaction --no-suggest" + ], + "l7": [ + "composer require laravel/framework:7.* orchestra/testbench:5.* --no-interaction --no-update", + "composer update --prefer-stable --prefer-dist --no-interaction --no-suggest" + ], + "l6": [ + "composer require laravel/framework:6.* orchestra/testbench:4.* --no-interaction --no-update", + "composer update --prefer-stable --prefer-dist --no-interaction --no-suggest" + ] } } diff --git a/src/CloudTasksJob.php b/src/CloudTasksJob.php index ddeea9d..b2109e5 100644 --- a/src/CloudTasksJob.php +++ b/src/CloudTasksJob.php @@ -111,5 +111,17 @@ public function release($delay = 0) parent::release(); $this->cloudTasksQueue->release($this, $delay); + + // The package uses the JobReleasedAfterException provided by Laravel to grab + // the payload of the released job in tests to easily run and test a released + // job. Because the event is only accessible in Laravel 9.x, we create an + // identical event to hook into for Laravel versions older than 9.x + if (version_compare(app()->version(), '9.0.0', '<')) { + $properties = TaskHandler::getCommandProperties($this->job['data']['command']); + + $connection = $properties['connection'] ?? config('queue.default'); + + app('events')->dispatch(new JobReleasedAfterException($connection, $this)); + } } } diff --git a/src/JobReleasedAfterException.php b/src/JobReleasedAfterException.php new file mode 100644 index 0000000..a916ff0 --- /dev/null +++ b/src/JobReleasedAfterException.php @@ -0,0 +1,37 @@ +job = $job; + $this->connectionName = $connectionName; + } +} diff --git a/tests/QueueTest.php b/tests/QueueTest.php index 7f44496..8149f8e 100644 --- a/tests/QueueTest.php +++ b/tests/QueueTest.php @@ -176,7 +176,9 @@ public function it_can_dispatch_after_commit_inline() DB::beginTransaction(); SimpleJob::dispatch()->afterCommit(); Event::assertNotDispatched(JobQueued::class); - DB::commit(); + while (DB::transactionLevel() !== 0) { + DB::commit(); + } Event::assertDispatched(JobQueued::class, function (JobQueued $event) { return $event->job instanceof SimpleJob; }); @@ -201,7 +203,9 @@ public function it_can_dispatch_after_commit_through_config() DB::beginTransaction(); SimpleJob::dispatch(); Event::assertNotDispatched(JobQueued::class); - DB::commit(); + while (DB::transactionLevel() !== 0) { + DB::commit(); + } Event::assertDispatched(JobQueued::class, function (JobQueued $event) { return $event->job instanceof SimpleJob; }); diff --git a/tests/TaskHandlerTest.php b/tests/TaskHandlerTest.php index 1a0b9eb..9ca6162 100644 --- a/tests/TaskHandlerTest.php +++ b/tests/TaskHandlerTest.php @@ -7,17 +7,14 @@ use Google\Protobuf\Duration; use Illuminate\Queue\Events\JobProcessed; use Illuminate\Queue\Events\JobProcessing; -use Illuminate\Queue\Events\JobReleasedAfterException; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Log; -use Illuminate\Validation\ValidationException; use Stackkit\LaravelGoogleCloudTasksQueue\CloudTasksApi; use Stackkit\LaravelGoogleCloudTasksQueue\CloudTasksException; use Stackkit\LaravelGoogleCloudTasksQueue\LogFake; use Stackkit\LaravelGoogleCloudTasksQueue\OpenIdVerificator; use Stackkit\LaravelGoogleCloudTasksQueue\StackkitCloudTask; use Stackkit\LaravelGoogleCloudTasksQueue\TaskHandler; -use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Tests\Support\EncryptedJob; use Tests\Support\FailingJob; use Tests\Support\SimpleJob; @@ -417,7 +414,7 @@ public function failing_jobs_are_released() CloudTasksApi::partialMock()->shouldReceive('getRetryConfig')->andReturn( (new RetryConfig())->setMaxAttempts(3) ); - Event::fake([JobReleasedAfterException::class]); + Event::fake($this->getJobReleasedAfterExceptionEvent()); // Act $job = $this->dispatch(new FailingJob()); @@ -431,7 +428,7 @@ public function failing_jobs_are_released() CloudTasksApi::assertDeletedTaskCount(1); CloudTasksApi::assertCreatedTaskCount(2); CloudTasksApi::assertTaskDeleted($job->task->getName()); - Event::assertDispatched(JobReleasedAfterException::class, function ($event) { + Event::assertDispatched($this->getJobReleasedAfterExceptionEvent(), function ($event) { return $event->job->attempts() === 1; }); } @@ -444,21 +441,21 @@ public function attempts_are_tracked_internally() // Arrange CloudTasksApi::fake(); OpenIdVerificator::fake(); - Event::fake([JobReleasedAfterException::class]); + Event::fake($this->getJobReleasedAfterExceptionEvent()); // Act & Assert $job = $this->dispatch(new FailingJob()); $job->run(); $releasedJob = null; - Event::assertDispatched(JobReleasedAfterException::class, function ($event) use (&$releasedJob) { + Event::assertDispatched($this->getJobReleasedAfterExceptionEvent(), function ($event) use (&$releasedJob) { $releasedJob = $event->job->getRawBody(); return $event->job->attempts() === 1; }); $this->runFromPayload($releasedJob); - Event::assertDispatched(JobReleasedAfterException::class, function ($event) { + Event::assertDispatched($this->getJobReleasedAfterExceptionEvent(), function ($event) { return $event->job->attempts() === 2; }); } diff --git a/tests/TestCase.php b/tests/TestCase.php index b7b3a61..e016c16 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -5,16 +5,13 @@ use Closure; use Firebase\JWT\JWT; use Google\ApiCore\ApiException; -use Google\Cloud\Tasks\V2\Queue; -use Google\Cloud\Tasks\V2\RetryConfig; use Google\Cloud\Tasks\V2\Task; use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Queue\Events\JobReleasedAfterException; use Illuminate\Support\Facades\DB; use Google\Cloud\Tasks\V2\CloudTasksClient; use Illuminate\Support\Facades\Event; -use Mockery; -use Stackkit\LaravelGoogleCloudTasksQueue\CloudTasksJob; +use Stackkit\LaravelGoogleCloudTasksQueue\JobReleasedAfterException as PackageJobReleasedAfterException; use Stackkit\LaravelGoogleCloudTasksQueue\TaskCreated; use Stackkit\LaravelGoogleCloudTasksQueue\TaskHandler; @@ -38,8 +35,8 @@ protected function setUp(): void $this->defaultHeaders['Authorization'] = 'Bearer ' . encrypt(time() + 10); Event::listen( - JobReleasedAfterException::class, - function (JobReleasedAfterException $event) { + $this->getJobReleasedAfterExceptionEvent(), + function ($event) { $this->releasedJobPayload = $event->job->getRawBody(); } ); @@ -238,4 +235,14 @@ protected function assertDatabaseCount($table, int $count, $connection = null) { $this->assertEquals($count, DB::connection($connection)->table($table)->count()); } + + public function getJobReleasedAfterExceptionEvent(): string + { + // The JobReleasedAfterException event is not available in Laravel versions + // below 9.x so instead for those versions we throw our own event which + // is identical to the Laravel one. + return version_compare(app()->version(), '9.0.0', '<') + ? PackageJobReleasedAfterException::class + : JobReleasedAfterException::class; + } } From a3e2c5575e0322c10d469bce71e48931e3d75e20 Mon Sep 17 00:00:00 2001 From: Marick van Tuil Date: Sun, 11 Sep 2022 14:24:15 +0200 Subject: [PATCH 07/18] Add JobReleased event and tests --- src/CloudTasksJob.php | 19 +++-- src/CloudTasksQueue.php | 5 +- src/CloudTasksServiceProvider.php | 5 ++ src/Events/JobReleased.php | 37 +++++++++ .../JobReleasedAfterException.php | 2 +- src/{ => Events}/TaskCreated.php | 2 +- tests/QueueTest.php | 76 +++++++++++++++++++ tests/Support/JobThatWillBeReleased.php | 38 ++++++++++ tests/TestCase.php | 6 +- 9 files changed, 176 insertions(+), 14 deletions(-) create mode 100644 src/Events/JobReleased.php rename src/{ => Events}/JobReleasedAfterException.php (91%) rename src/{ => Events}/TaskCreated.php (86%) create mode 100644 tests/Support/JobThatWillBeReleased.php diff --git a/src/CloudTasksJob.php b/src/CloudTasksJob.php index b2109e5..35c31ff 100644 --- a/src/CloudTasksJob.php +++ b/src/CloudTasksJob.php @@ -3,8 +3,10 @@ namespace Stackkit\LaravelGoogleCloudTasksQueue; use Illuminate\Container\Container; -use Illuminate\Queue\Jobs\Job as LaravelJob; use Illuminate\Contracts\Queue\Job as JobContract; +use Illuminate\Queue\Jobs\Job as LaravelJob; +use Stackkit\LaravelGoogleCloudTasksQueue\Events\JobReleased; +use Stackkit\LaravelGoogleCloudTasksQueue\Events\JobReleasedAfterException; use function Safe\json_encode; class CloudTasksJob extends LaravelJob implements JobContract @@ -14,7 +16,7 @@ class CloudTasksJob extends LaravelJob implements JobContract * * @var array */ - private array $job; + public array $job; private ?int $maxTries; public ?int $retryUntil = null; @@ -112,16 +114,21 @@ public function release($delay = 0) $this->cloudTasksQueue->release($this, $delay); + $properties = TaskHandler::getCommandProperties($this->job['data']['command']); + $connection = $properties['connection'] ?? config('queue.default'); + // The package uses the JobReleasedAfterException provided by Laravel to grab // the payload of the released job in tests to easily run and test a released // job. Because the event is only accessible in Laravel 9.x, we create an // identical event to hook into for Laravel versions older than 9.x if (version_compare(app()->version(), '9.0.0', '<')) { - $properties = TaskHandler::getCommandProperties($this->job['data']['command']); - - $connection = $properties['connection'] ?? config('queue.default'); + if (data_get($this->job, 'internal.errored')) { + app('events')->dispatch(new JobReleasedAfterException($connection, $this)); + } + } - app('events')->dispatch(new JobReleasedAfterException($connection, $this)); + if (! data_get($this->job, 'internal.errored')) { + app('events')->dispatch(new JobReleased($connection, $this)); } } } diff --git a/src/CloudTasksQueue.php b/src/CloudTasksQueue.php index 6b3b5ac..a77e09c 100644 --- a/src/CloudTasksQueue.php +++ b/src/CloudTasksQueue.php @@ -12,8 +12,9 @@ use Illuminate\Contracts\Queue\Queue as QueueContract; use Illuminate\Queue\Queue as LaravelQueue; use Illuminate\Support\Str; -use function Safe\json_encode; +use Stackkit\LaravelGoogleCloudTasksQueue\Events\TaskCreated; use function Safe\json_decode; +use function Safe\json_encode; class CloudTasksQueue extends LaravelQueue implements QueueContract { @@ -197,8 +198,6 @@ private function withAttempts(string $payload): string if (!isset($decoded['internal']['attempts'])) { $decoded['internal']['attempts'] = 0; - } else { - $decoded['internal']['attempts']++; } return json_encode($decoded); diff --git a/src/CloudTasksServiceProvider.php b/src/CloudTasksServiceProvider.php index 07b3c6f..67e6a32 100644 --- a/src/CloudTasksServiceProvider.php +++ b/src/CloudTasksServiceProvider.php @@ -8,6 +8,7 @@ use Illuminate\Queue\Events\JobProcessed; use Illuminate\Queue\Events\JobProcessing; use Illuminate\Support\ServiceProvider as LaravelServiceProvider; +use Stackkit\LaravelGoogleCloudTasksQueue\Events\TaskCreated; use function Safe\file_get_contents; use function Safe\json_decode; @@ -158,6 +159,8 @@ private function registerDashboard(): void }); app('events')->listen(JobProcessed::class, function (JobProcessed $event) { + data_set($event->job->job, 'internal.processed', true); + if (!CloudTasks::dashboardEnabled()) { return; } @@ -168,6 +171,8 @@ private function registerDashboard(): void }); app('events')->listen(JobExceptionOccurred::class, function (JobExceptionOccurred $event) { + data_set($event->job->job, 'internal.errored', true); + if (!CloudTasks::dashboardEnabled()) { return; } diff --git a/src/Events/JobReleased.php b/src/Events/JobReleased.php new file mode 100644 index 0000000..d6af0fa --- /dev/null +++ b/src/Events/JobReleased.php @@ -0,0 +1,37 @@ +job = $job; + $this->connectionName = $connectionName; + } +} diff --git a/src/JobReleasedAfterException.php b/src/Events/JobReleasedAfterException.php similarity index 91% rename from src/JobReleasedAfterException.php rename to src/Events/JobReleasedAfterException.php index a916ff0..603fbe3 100644 --- a/src/JobReleasedAfterException.php +++ b/src/Events/JobReleasedAfterException.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Stackkit\LaravelGoogleCloudTasksQueue; +namespace Stackkit\LaravelGoogleCloudTasksQueue\Events; use Illuminate\Contracts\Queue\Job; diff --git a/src/TaskCreated.php b/src/Events/TaskCreated.php similarity index 86% rename from src/TaskCreated.php rename to src/Events/TaskCreated.php index 96f0f45..a05f415 100644 --- a/src/TaskCreated.php +++ b/src/Events/TaskCreated.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Stackkit\LaravelGoogleCloudTasksQueue; +namespace Stackkit\LaravelGoogleCloudTasksQueue\Events; use Google\Cloud\Tasks\V2\Task; diff --git a/tests/QueueTest.php b/tests/QueueTest.php index 8149f8e..0a8de46 100644 --- a/tests/QueueTest.php +++ b/tests/QueueTest.php @@ -7,11 +7,15 @@ use Google\Cloud\Tasks\V2\HttpMethod; use Google\Cloud\Tasks\V2\Task; use Illuminate\Queue\Events\JobQueued; +use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Event; use Stackkit\LaravelGoogleCloudTasksQueue\CloudTasksApi; +use Stackkit\LaravelGoogleCloudTasksQueue\Events\JobReleased; +use Stackkit\LaravelGoogleCloudTasksQueue\OpenIdVerificator; use Stackkit\LaravelGoogleCloudTasksQueue\TaskHandler; use Tests\Support\FailingJob; +use Tests\Support\JobThatWillBeReleased; use Tests\Support\SimpleJob; class QueueTest extends TestCase @@ -210,4 +214,76 @@ public function it_can_dispatch_after_commit_through_config() return $event->job instanceof SimpleJob; }); } + + /** + * @test + */ + public function jobs_can_be_released() + { + // Arrange + CloudTasksApi::fake(); + OpenIdVerificator::fake(); + Event::fake([ + $this->getJobReleasedAfterExceptionEvent(), + JobReleased::class, + ]); + + // Act + $this->dispatch(new JobThatWillBeReleased())->run(); + + // Assert + Event::assertNotDispatched($this->getJobReleasedAfterExceptionEvent()); + CloudTasksApi::assertDeletedTaskCount(1); + $releasedJob = null; + Event::assertDispatched(JobReleased::class, function (JobReleased $event) use (&$releasedJob) { + $releasedJob = $event->job; + return true; + }); + CloudTasksApi::assertTaskCreated(function (Task $task) { + $body = $task->getHttpRequest()->getBody(); + $decoded = json_decode($body, true); + return $decoded['data']['commandName'] === 'Tests\\Support\\JobThatWillBeReleased' + && $decoded['internal']['attempts'] === 1; + }); + + $this->runFromPayload($releasedJob->getRawBody()); + + CloudTasksApi::assertDeletedTaskCount(2); + CloudTasksApi::assertTaskCreated(function (Task $task) { + $body = $task->getHttpRequest()->getBody(); + $decoded = json_decode($body, true); + return $decoded['data']['commandName'] === 'Tests\\Support\\JobThatWillBeReleased' + && $decoded['internal']['attempts'] === 2; + }); + } + + /** + * @test + */ + public function jobs_can_be_released_with_a_delay() + { + // Arrange + CloudTasksApi::fake(); + OpenIdVerificator::fake(); + Event::fake([ + $this->getJobReleasedAfterExceptionEvent(), + JobReleased::class, + ]); + Carbon::setTestNow(now()->addDay()); + + // Act + $this->dispatch(new JobThatWillBeReleased(15))->run(); + + // Assert + CloudTasksApi::assertTaskCreated(function (Task $task) { + $body = $task->getHttpRequest()->getBody(); + $decoded = json_decode($body, true); + + $scheduleTime = $task->getScheduleTime() ? $task->getScheduleTime()->getSeconds() : null; + + return $decoded['data']['commandName'] === 'Tests\\Support\\JobThatWillBeReleased' + && $decoded['internal']['attempts'] === 1 + && $scheduleTime === now()->getTimestamp() + 15; + }); + } } diff --git a/tests/Support/JobThatWillBeReleased.php b/tests/Support/JobThatWillBeReleased.php new file mode 100644 index 0000000..bbd2d98 --- /dev/null +++ b/tests/Support/JobThatWillBeReleased.php @@ -0,0 +1,38 @@ +releaseDelay = $releaseDelay; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + logger('JobThatWillBeReleased:beforeRelease'); + $this->release($this->releaseDelay); + logger('JobThatWillBeReleased:afterRelease'); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index e016c16..9110869 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -5,14 +5,14 @@ use Closure; use Firebase\JWT\JWT; use Google\ApiCore\ApiException; +use Google\Cloud\Tasks\V2\CloudTasksClient; use Google\Cloud\Tasks\V2\Task; use Illuminate\Foundation\Testing\DatabaseTransactions; use Illuminate\Queue\Events\JobReleasedAfterException; use Illuminate\Support\Facades\DB; -use Google\Cloud\Tasks\V2\CloudTasksClient; use Illuminate\Support\Facades\Event; -use Stackkit\LaravelGoogleCloudTasksQueue\JobReleasedAfterException as PackageJobReleasedAfterException; -use Stackkit\LaravelGoogleCloudTasksQueue\TaskCreated; +use Stackkit\LaravelGoogleCloudTasksQueue\Events\JobReleasedAfterException as PackageJobReleasedAfterException; +use Stackkit\LaravelGoogleCloudTasksQueue\Events\TaskCreated; use Stackkit\LaravelGoogleCloudTasksQueue\TaskHandler; class TestCase extends \Orchestra\Testbench\TestCase From 728537dd49dace5d68d7141d56cce86bd66aed28 Mon Sep 17 00:00:00 2001 From: Marick van Tuil Date: Sun, 11 Sep 2022 15:49:07 +0200 Subject: [PATCH 08/18] Add initial version of job releasing in dashboard --- dashboard/src/components/Status.vue | 2 +- dashboard/src/components/Task.vue | 8 ++++++++ src/CloudTasksJob.php | 2 +- src/CloudTasksServiceProvider.php | 9 +++++++++ src/DashboardService.php | 22 ++++++++++++++++++++ src/Events/JobReleased.php | 11 +++++++++- src/TaskHandler.php | 2 +- tests/CloudTasksDashboardTest.php | 31 +++++++++++++++++++++++++++++ 8 files changed, 83 insertions(+), 4 deletions(-) diff --git a/dashboard/src/components/Status.vue b/dashboard/src/components/Status.vue index e542088..b1beb73 100644 --- a/dashboard/src/components/Status.vue +++ b/dashboard/src/components/Status.vue @@ -31,7 +31,7 @@ function ucfirst(input) { .task-queued, .task-scheduled { @apply bg-gray-100 text-gray-500 } -.task-running { +.task-running, .task-released { @apply bg-blue-100 text-blue-800 } diff --git a/dashboard/src/components/Task.vue b/dashboard/src/components/Task.vue index b8297ea..031dd73 100644 --- a/dashboard/src/components/Task.vue +++ b/dashboard/src/components/Task.vue @@ -27,6 +27,7 @@ const titles = { successful: 'Successful', error: 'An error occurred', failed: 'Failed permanently', + released: 'Released', } @@ -60,6 +61,13 @@ const titles = { Scheduled: {{ event['scheduled_at'] }} (UTC) +
+ + Delay: {{ event['delay'] }} seconds + +
job, 'internal.errored')) { - app('events')->dispatch(new JobReleased($connection, $this)); + app('events')->dispatch(new JobReleased($connection, $this, $delay)); } } } diff --git a/src/CloudTasksServiceProvider.php b/src/CloudTasksServiceProvider.php index 67e6a32..ebe922d 100644 --- a/src/CloudTasksServiceProvider.php +++ b/src/CloudTasksServiceProvider.php @@ -8,6 +8,7 @@ use Illuminate\Queue\Events\JobProcessed; use Illuminate\Queue\Events\JobProcessing; use Illuminate\Support\ServiceProvider as LaravelServiceProvider; +use Stackkit\LaravelGoogleCloudTasksQueue\Events\JobReleased; use Stackkit\LaravelGoogleCloudTasksQueue\Events\TaskCreated; use function Safe\file_get_contents; use function Safe\json_decode; @@ -187,5 +188,13 @@ private function registerDashboard(): void DashboardService::make()->markAsFailed($event); }); + + app('events')->listen(JobReleased::class, function (JobReleased $event) { + if (!CloudTasks::dashboardEnabled()) { + return; + } + + DashboardService::make()->markAsReleased($event); + }); } } diff --git a/src/DashboardService.php b/src/DashboardService.php index cb33ffd..8800e3d 100644 --- a/src/DashboardService.php +++ b/src/DashboardService.php @@ -9,6 +9,7 @@ use Illuminate\Queue\Events\JobExceptionOccurred; use Illuminate\Queue\Events\JobFailed; use Illuminate\Support\Facades\DB; +use Stackkit\LaravelGoogleCloudTasksQueue\Events\JobReleased; use function Safe\json_decode; class DashboardService @@ -85,6 +86,10 @@ public function markAsSuccessful(string $uuid): void { $task = StackkitCloudTask::findByUuid($uuid); + if ($task->status === 'released') { + return; + } + $task->status = 'successful'; $task->addMetadataEvent([ 'status' => $task->status, @@ -135,6 +140,23 @@ public function markAsFailed(JobFailed $event): void $task->save(); } + public function markAsReleased(JobReleased $event): void + { + /** @var CloudTasksJob $job */ + $job = $event->job; + + $task = StackkitCloudTask::findByUuid($job->uuid()); + + $task->status = 'released'; + $task->addMetadataEvent([ + 'status' => $task->status, + 'datetime' => now()->utc()->toDateTimeString(), + 'delay' => $event->delay, + ]); + + $task->save(); + } + private function getTaskName(Task $task): string { /** @var array $decode */ diff --git a/src/Events/JobReleased.php b/src/Events/JobReleased.php index d6af0fa..614e45b 100644 --- a/src/Events/JobReleased.php +++ b/src/Events/JobReleased.php @@ -22,16 +22,25 @@ class JobReleased */ public Job $job; + /** + * The job delay in seconds. + * + * @var int + */ + public int $delay; + /** * Create a new event instance. * * @param string $connectionName * @param Job $job + * @param int $delay * @return void */ - public function __construct(string $connectionName, Job $job) + public function __construct(string $connectionName, Job $job, int $delay = 0) { $this->job = $job; $this->connectionName = $connectionName; + $this->delay = $delay; } } diff --git a/src/TaskHandler.php b/src/TaskHandler.php index 41cf9a4..8197684 100644 --- a/src/TaskHandler.php +++ b/src/TaskHandler.php @@ -166,7 +166,7 @@ public static function getCommandProperties(string $command): array } if (app()->bound(Encrypter::class)) { - return (array) unserialize(app(Encrypter::class)->decrypt($command), ['allowed_classes' => false]); + return (array) unserialize(app(Encrypter::class)->decrypt($command), ['allowed_classes' => ['Illuminate\Support\Carbon']]); } return []; diff --git a/tests/CloudTasksDashboardTest.php b/tests/CloudTasksDashboardTest.php index 373267e..8f09539 100644 --- a/tests/CloudTasksDashboardTest.php +++ b/tests/CloudTasksDashboardTest.php @@ -10,6 +10,7 @@ use Stackkit\LaravelGoogleCloudTasksQueue\OpenIdVerificator; use Stackkit\LaravelGoogleCloudTasksQueue\StackkitCloudTask; use Tests\Support\FailingJob; +use Tests\Support\JobThatWillBeReleased; use Tests\Support\SimpleJob; class CloudTasksDashboardTest extends TestCase @@ -413,6 +414,36 @@ public function when_a_job_fails_it_will_be_updated_in_the_dashboard() ); } + /** + * @test + */ + public function when_a_job_is_released_it_will_be_updated_in_the_dashboard() + { + // Arrange + \Illuminate\Support\Carbon::setTestNow(now()); + CloudTasksApi::fake(); + OpenIdVerificator::fake(); + CloudTasksApi::partialMock()->shouldReceive('getRetryConfig')->andReturn( + (new RetryConfig())->setMaxAttempts(3) + ); + + $this->dispatch(new JobThatWillBeReleased())->run(); + + // Assert + $task = StackkitCloudTask::firstOrFail(); + $events = $task->getEvents(); + + $this->assertCount(4, $events); + $this->assertEquals( + [ + 'status' => 'released', + 'datetime' => now()->toDateTimeString(), + 'diff' => '1 second ago', + ], + $events[2] + ); + } + /** * @test */ From 2ab4fbfb3c0700f5947844aeafb501b5ec415347 Mon Sep 17 00:00:00 2001 From: Marick van Tuil Date: Sun, 11 Sep 2022 16:42:03 +0200 Subject: [PATCH 09/18] Fix test --- tests/CloudTasksDashboardTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/CloudTasksDashboardTest.php b/tests/CloudTasksDashboardTest.php index 8f09539..4abb86f 100644 --- a/tests/CloudTasksDashboardTest.php +++ b/tests/CloudTasksDashboardTest.php @@ -433,7 +433,7 @@ public function when_a_job_is_released_it_will_be_updated_in_the_dashboard() $task = StackkitCloudTask::firstOrFail(); $events = $task->getEvents(); - $this->assertCount(4, $events); + $this->assertCount(3, $events); $this->assertEquals( [ 'status' => 'released', From c387678f3893c025ea319062c87743cae8500d83 Mon Sep 17 00:00:00 2001 From: Marick van Tuil Date: Sun, 11 Sep 2022 16:45:48 +0200 Subject: [PATCH 10/18] Add extra release delay test --- tests/CloudTasksDashboardTest.php | 32 +++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/CloudTasksDashboardTest.php b/tests/CloudTasksDashboardTest.php index 4abb86f..6cb7b99 100644 --- a/tests/CloudTasksDashboardTest.php +++ b/tests/CloudTasksDashboardTest.php @@ -439,6 +439,38 @@ public function when_a_job_is_released_it_will_be_updated_in_the_dashboard() 'status' => 'released', 'datetime' => now()->toDateTimeString(), 'diff' => '1 second ago', + 'delay' => 0, + ], + $events[2] + ); + } + + /** + * @test + */ + public function job_release_delay_is_added_to_the_metadata() + { + // Arrange + \Illuminate\Support\Carbon::setTestNow(now()); + CloudTasksApi::fake(); + OpenIdVerificator::fake(); + CloudTasksApi::partialMock()->shouldReceive('getRetryConfig')->andReturn( + (new RetryConfig())->setMaxAttempts(3) + ); + + $this->dispatch(new JobThatWillBeReleased(15))->run(); + + // Assert + $task = StackkitCloudTask::firstOrFail(); + $events = $task->getEvents(); + + $this->assertCount(3, $events); + $this->assertEquals( + [ + 'status' => 'released', + 'datetime' => now()->toDateTimeString(), + 'diff' => '1 second ago', + 'delay' => 15, ], $events[2] ); From 8a19e95d812782dead7b9c4a502da67279acfc22 Mon Sep 17 00:00:00 2001 From: Marick van Tuil Date: Sun, 11 Sep 2022 17:45:47 +0200 Subject: [PATCH 11/18] Expand test suite --- README.md | 1 + src/LogFake.php | 8 + src/TaskHandler.php | 11 +- tests/QueueTest.php | 161 ++++++++++++++++++ tests/Support/FailingJob.php | 5 + .../FailingJobWithExponentialBackoff.php | 24 +++ 6 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 tests/Support/FailingJobWithExponentialBackoff.php diff --git a/README.md b/README.md index ee2f96e..bb824a3 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ Please check the table below for supported Laravel and PHP versions: // does not respond by this deadline then the request is cancelled and the attempt // is marked as a DEADLINE_EXCEEDED failure. 'dispatch_deadline' => null, + 'backoff' => 0, ], ``` diff --git a/src/LogFake.php b/src/LogFake.php index d9a32c6..e4e86ff 100644 --- a/src/LogFake.php +++ b/src/LogFake.php @@ -68,4 +68,12 @@ public function assertLogged(string $message): void { PHPUnit::assertTrue(in_array($message, $this->loggedMessages), 'The message [' . $message . '] was not logged.'); } + + public function assertNotLogged(string $message): void + { + PHPUnit::assertTrue( + ! in_array($message, $this->loggedMessages), + 'The message [' . $message . '] was logged.' + ); + } } diff --git a/src/TaskHandler.php b/src/TaskHandler.php index 8197684..5bbdc7b 100644 --- a/src/TaskHandler.php +++ b/src/TaskHandler.php @@ -147,7 +147,7 @@ private function handleTask(array $task): void $job->setAttempts($job->attempts() + 1); - app('queue.worker')->process($this->config['connection'], $job, new WorkerOptions()); + app('queue.worker')->process($this->config['connection'], $job, $this->getWorkerOptions()); } private function loadQueueRetryConfig(CloudTasksJob $job): void @@ -171,4 +171,13 @@ public static function getCommandProperties(string $command): array return []; } + + public function getWorkerOptions(): WorkerOptions + { + $options = new WorkerOptions(); + + $options->backoff = $this->config['backoff'] ?? 0; + + return $options; + } } diff --git a/tests/QueueTest.php b/tests/QueueTest.php index 0a8de46..e1dd019 100644 --- a/tests/QueueTest.php +++ b/tests/QueueTest.php @@ -5,16 +5,23 @@ namespace Tests; use Google\Cloud\Tasks\V2\HttpMethod; +use Google\Cloud\Tasks\V2\RetryConfig; use Google\Cloud\Tasks\V2\Task; +use Illuminate\Queue\Events\JobProcessed; +use Illuminate\Queue\Events\JobProcessing; use Illuminate\Queue\Events\JobQueued; use Illuminate\Support\Carbon; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Event; +use Illuminate\Support\Facades\Log; +use Illuminate\Support\Facades\Queue; use Stackkit\LaravelGoogleCloudTasksQueue\CloudTasksApi; use Stackkit\LaravelGoogleCloudTasksQueue\Events\JobReleased; +use Stackkit\LaravelGoogleCloudTasksQueue\LogFake; use Stackkit\LaravelGoogleCloudTasksQueue\OpenIdVerificator; use Stackkit\LaravelGoogleCloudTasksQueue\TaskHandler; use Tests\Support\FailingJob; +use Tests\Support\FailingJobWithExponentialBackoff; use Tests\Support\JobThatWillBeReleased; use Tests\Support\SimpleJob; @@ -286,4 +293,158 @@ public function jobs_can_be_released_with_a_delay() && $scheduleTime === now()->getTimestamp() + 15; }); } + + /** @test */ + public function test_default_backoff() + { + // Arrange + CloudTasksApi::fake(); + OpenIdVerificator::fake(); + Event::fake($this->getJobReleasedAfterExceptionEvent()); + + // Act + $this->dispatch(new FailingJob())->run(); + + // Assert + CloudTasksApi::assertTaskCreated(function (Task $task) { + return is_null($task->getScheduleTime()); + }); + } + + /** @test */ + public function test_backoff_from_queue_config() + { + // Arrange + Carbon::setTestNow(now()->addDay()); + $this->setConfigValue('backoff', 123); + CloudTasksApi::fake(); + OpenIdVerificator::fake(); + Event::fake($this->getJobReleasedAfterExceptionEvent()); + + // Act + $this->dispatch(new FailingJob())->run(); + + // Assert + CloudTasksApi::assertTaskCreated(function (Task $task) { + return $task->getScheduleTime() + && $task->getScheduleTime()->getSeconds() === now()->getTimestamp() + 123; + }); + } + + /** @test */ + public function test_backoff_from_job() + { + // Arrange + Carbon::setTestNow(now()->addDay()); + CloudTasksApi::fake(); + OpenIdVerificator::fake(); + Event::fake($this->getJobReleasedAfterExceptionEvent()); + + // Act + $failingJob = new FailingJob(); + $failingJob->backoff = 123; + $this->dispatch($failingJob)->run(); + + // Assert + CloudTasksApi::assertTaskCreated(function (Task $task) { + return $task->getScheduleTime() + && $task->getScheduleTime()->getSeconds() === now()->getTimestamp() + 123; + }); + } + + /** @test */ + public function test_exponential_backoff_from_job_method() + { + // Arrange + Carbon::setTestNow(now()->addDay()); + CloudTasksApi::fake(); + OpenIdVerificator::fake(); + + // Act + $releasedJob = $this->dispatch(new FailingJobWithExponentialBackoff()) + ->runAndGetReleasedJob(); + $releasedJob = $releasedJob->runAndGetReleasedJob(); + $releasedJob->run(); + + // Assert + CloudTasksApi::assertTaskCreated(function (Task $task) { + return $task->getScheduleTime() + && $task->getScheduleTime()->getSeconds() === now()->getTimestamp() + 50; + }); + CloudTasksApi::assertTaskCreated(function (Task $task) { + return $task->getScheduleTime() + && $task->getScheduleTime()->getSeconds() === now()->getTimestamp() + 60; + }); + CloudTasksApi::assertTaskCreated(function (Task $task) { + return $task->getScheduleTime() + && $task->getScheduleTime()->getSeconds() === now()->getTimestamp() + 70; + }); + } + + /** @test */ + public function test_failing_method_on_job() + { + // Arrange + CloudTasksApi::fake(); + CloudTasksApi::partialMock()->shouldReceive('getRetryConfig')->andReturn( + // -1 is a valid option in Cloud Tasks to indicate there is no max. + (new RetryConfig())->setMaxAttempts(1) + ); + + OpenIdVerificator::fake(); + Log::swap(new LogFake()); + + // Act + $this->dispatch(new FailingJob())->run(); + + // Assert + Log::assertLogged('FailingJob:failed'); + } + + /** @test */ + public function test_queue_before_and_after_hooks() + { + // Arrange + CloudTasksApi::fake(); + OpenIdVerificator::fake(); + Log::swap(new LogFake()); + + // Act + Queue::before(function (JobProcessing $event) { + logger('Queue::before:' . $event->job->payload()['data']['commandName']); + }); + Queue::after(function (JobProcessed $event) { + logger('Queue::after:' . $event->job->payload()['data']['commandName']); + }); + $this->dispatch(new SimpleJob())->run(); + + // Assert + Log::assertLogged('Queue::before:Tests\Support\SimpleJob'); + Log::assertLogged('Queue::after:Tests\Support\SimpleJob'); + } + + /** @test */ + public function test_queue_looping_hook_not_supported_with_this_package() + { + // Arrange + CloudTasksApi::fake(); + OpenIdVerificator::fake(); + Log::swap(new LogFake()); + + // Act + Queue::looping(function () { + logger('Queue::looping'); + }); + $this->dispatch(new SimpleJob())->run(); + + // Assert + Log::assertNotLogged('Queue::looping'); + } + + /** @test */ + public function test_ignoring_jobs_with_deleted_models() + { + // todo + $this->assertTrue(true); + } } diff --git a/tests/Support/FailingJob.php b/tests/Support/FailingJob.php index f8d9e87..5fbbffc 100644 --- a/tests/Support/FailingJob.php +++ b/tests/Support/FailingJob.php @@ -31,4 +31,9 @@ public function handle() { throw new \Error('simulating a failing job'); } + + public function failed(\Throwable $throwable) + { + logger('FailingJob:failed'); + } } diff --git a/tests/Support/FailingJobWithExponentialBackoff.php b/tests/Support/FailingJobWithExponentialBackoff.php new file mode 100644 index 0000000..aec644d --- /dev/null +++ b/tests/Support/FailingJobWithExponentialBackoff.php @@ -0,0 +1,24 @@ + Date: Fri, 16 Sep 2022 19:47:53 +0200 Subject: [PATCH 12/18] Test $deleteWhenMissingModels --- tests/QueueTest.php | 31 +++++++++++++++++++++++++++++-- tests/Support/User.php | 12 ++++++++++++ tests/Support/UserJob.php | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 tests/Support/User.php create mode 100644 tests/Support/UserJob.php diff --git a/tests/QueueTest.php b/tests/QueueTest.php index e1dd019..6c595ae 100644 --- a/tests/QueueTest.php +++ b/tests/QueueTest.php @@ -24,6 +24,8 @@ use Tests\Support\FailingJobWithExponentialBackoff; use Tests\Support\JobThatWillBeReleased; use Tests\Support\SimpleJob; +use Tests\Support\User; +use Tests\Support\UserJob; class QueueTest extends TestCase { @@ -444,7 +446,32 @@ public function test_queue_looping_hook_not_supported_with_this_package() /** @test */ public function test_ignoring_jobs_with_deleted_models() { - // todo - $this->assertTrue(true); + // Arrange + CloudTasksApi::fake(); + OpenIdVerificator::fake(); + Log::swap(new LogFake()); + + $user1 = User::create([ + 'name' => 'John', + 'email' => 'johndoe@example.com', + 'password' => bcrypt('test'), + ]); + + $user2 = User::create([ + 'name' => 'Jane', + 'email' => 'janedoe@example.com', + 'password' => bcrypt('test'), + ]); + + // Act + $this->dispatch(new UserJob($user1))->runWithoutExceptionHandler(); + + $job = $this->dispatch(new UserJob($user2)); + $user2->delete(); + $job->runWithoutExceptionHandler(); + + // Act + Log::assertLogged('UserJob:John'); + CloudTasksApi::assertTaskDeleted($job->task->getName()); } } diff --git a/tests/Support/User.php b/tests/Support/User.php new file mode 100644 index 0000000..7ffec22 --- /dev/null +++ b/tests/Support/User.php @@ -0,0 +1,12 @@ +user = $user; + } + + /** + * Execute the job. + * + * @return void + */ + public function handle() + { + logger('UserJob:' . $this->user->name); + } +} From 281a0772683cd3f9d5ad8845796237c3757b5c2f Mon Sep 17 00:00:00 2001 From: Marick van Tuil Date: Fri, 16 Sep 2022 21:32:12 +0200 Subject: [PATCH 13/18] Provide fallback for older style jobs and ensure their attempt number is not lost --- src/TaskHandler.php | 11 ++++++++++- tests/QueueTest.php | 1 - tests/TaskHandlerTest.php | 20 +++++++++++++++++++- 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/TaskHandler.php b/src/TaskHandler.php index 5bbdc7b..022c694 100644 --- a/src/TaskHandler.php +++ b/src/TaskHandler.php @@ -122,7 +122,16 @@ private function handleTask(array $task): void $this->loadQueueRetryConfig($job); - $job->setAttempts($task['internal']['attempts']); + // If the task has a [X-CloudTasks-TaskRetryCount] header higher than 0, then + // we know the job was created using an earlier version of the package. This + // job does not have the attempts tracked internally yet. + $taskRetryCountHeader = request()->header('X-CloudTasks-TaskRetryCount'); + if ($taskRetryCountHeader && (int) $taskRetryCountHeader > 0) { + $job->setAttempts((int) $taskRetryCountHeader); + } else { + $job->setAttempts($task['internal']['attempts']); + } + $job->setMaxTries($this->retryConfig->getMaxAttempts()); // If the job is being attempted again we also check if a diff --git a/tests/QueueTest.php b/tests/QueueTest.php index 6c595ae..1fad689 100644 --- a/tests/QueueTest.php +++ b/tests/QueueTest.php @@ -389,7 +389,6 @@ public function test_failing_method_on_job() // Arrange CloudTasksApi::fake(); CloudTasksApi::partialMock()->shouldReceive('getRetryConfig')->andReturn( - // -1 is a valid option in Cloud Tasks to indicate there is no max. (new RetryConfig())->setMaxAttempts(1) ); diff --git a/tests/TaskHandlerTest.php b/tests/TaskHandlerTest.php index 9ca6162..d92f66c 100644 --- a/tests/TaskHandlerTest.php +++ b/tests/TaskHandlerTest.php @@ -439,7 +439,6 @@ public function failing_jobs_are_released() public function attempts_are_tracked_internally() { // Arrange - CloudTasksApi::fake(); OpenIdVerificator::fake(); Event::fake($this->getJobReleasedAfterExceptionEvent()); @@ -459,4 +458,23 @@ public function attempts_are_tracked_internally() return $event->job->attempts() === 2; }); } + + /** + * @test + */ + public function attempts_are_copied_from_x_header() + { + // Arrange + OpenIdVerificator::fake(); + Event::fake([JobProcessing::class]); + + // Act & Assert + $job = $this->dispatch(new SimpleJob()); + request()->headers->set('X-CloudTasks-TaskRetryCount', 6); + $job->run(); + + Event::assertDispatched(JobProcessing::class, function (JobProcessing $event) { + return $event->job->attempts() === 7; + }); + } } From c3b98bdf1fe435f6cf8ec320497ea190e4286588 Mon Sep 17 00:00:00 2001 From: Marick van Tuil Date: Fri, 16 Sep 2022 22:46:44 +0200 Subject: [PATCH 14/18] Fix test --- tests/TaskHandlerTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/TaskHandlerTest.php b/tests/TaskHandlerTest.php index d92f66c..36a2d13 100644 --- a/tests/TaskHandlerTest.php +++ b/tests/TaskHandlerTest.php @@ -466,14 +466,14 @@ public function attempts_are_copied_from_x_header() { // Arrange OpenIdVerificator::fake(); - Event::fake([JobProcessing::class]); + Event::fake($this->getJobReleasedAfterExceptionEvent()); // Act & Assert - $job = $this->dispatch(new SimpleJob()); + $job = $this->dispatch(new FailingJob()); request()->headers->set('X-CloudTasks-TaskRetryCount', 6); $job->run(); - Event::assertDispatched(JobProcessing::class, function (JobProcessing $event) { + Event::assertDispatched($this->getJobReleasedAfterExceptionEvent(), function ($event) { return $event->job->attempts() === 7; }); } From 71a92fedd1ba86c50784e915ecfca7a2c4300245 Mon Sep 17 00:00:00 2001 From: Marick van Tuil Date: Fri, 16 Sep 2022 23:11:59 +0200 Subject: [PATCH 15/18] Run all tests in Github Actions --- phpunit.xml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/phpunit.xml b/phpunit.xml index 8ab5281..4f23d86 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -10,10 +10,7 @@ stopOnFailure="false"> - ./tests/ConfigTest.php - ./tests/TaskHandlerTest.php - ./tests/CloudTasksApiTest.php - ./tests/CloudTasksDashboardTest.php + ./tests From e3e01c6e7128361e978c79b6220b15b3f7356606 Mon Sep 17 00:00:00 2001 From: Marick van Tuil Date: Fri, 16 Sep 2022 23:23:55 +0200 Subject: [PATCH 16/18] Use delay on Laravel 7x and below, backoff on 8.x and up --- src/TaskHandler.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/TaskHandler.php b/src/TaskHandler.php index 022c694..a535f30 100644 --- a/src/TaskHandler.php +++ b/src/TaskHandler.php @@ -185,7 +185,9 @@ public function getWorkerOptions(): WorkerOptions { $options = new WorkerOptions(); - $options->backoff = $this->config['backoff'] ?? 0; + $prop = version_compare(app()->version(), '8.0.0', '<') ? 'delay' : 'backoff'; + + $options->$prop = $this->config['backoff'] ?? 0; return $options; } From 71f67218c054e486af1e000f7a708b106c24a676 Mon Sep 17 00:00:00 2001 From: Marick van Tuil Date: Fri, 16 Sep 2022 23:31:52 +0200 Subject: [PATCH 17/18] Skip unsupported exponential backoff on 7.x and below --- tests/QueueTest.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/QueueTest.php b/tests/QueueTest.php index 1fad689..4a019b9 100644 --- a/tests/QueueTest.php +++ b/tests/QueueTest.php @@ -344,7 +344,8 @@ public function test_backoff_from_job() // Act $failingJob = new FailingJob(); - $failingJob->backoff = 123; + $prop = version_compare(app()->version(), '8.0.0', '<') ? 'delay' : 'backoff'; + $failingJob->$prop = 123; $this->dispatch($failingJob)->run(); // Assert @@ -357,6 +358,10 @@ public function test_backoff_from_job() /** @test */ public function test_exponential_backoff_from_job_method() { + if (version_compare(app()->version(), '8.0.0', '<')) { + $this->markTestSkipped('Not supported by Laravel 7.x and below.'); + } + // Arrange Carbon::setTestNow(now()->addDay()); CloudTasksApi::fake(); From 90b68f66aedbc068f9a2b0c873dfc0cb188d23e5 Mon Sep 17 00:00:00 2001 From: Marick van Tuil Date: Fri, 30 Sep 2022 14:25:05 +0200 Subject: [PATCH 18/18] Make dashboard build --- .../{index.1002db9a.css => index.d8eef428.css} | 2 +- .../{index.5a46c6a0.js => index.ea68d73f.js} | 2 +- dashboard/dist/crossword.png | Bin 43694 -> 0 bytes dashboard/dist/dot-grid.png | Bin 25993 -> 0 bytes dashboard/dist/index.html | 4 ++-- dashboard/dist/manifest.json | 4 ++-- dashboard/dist/pw_maze_white.png | Bin 600 -> 0 bytes 7 files changed, 6 insertions(+), 6 deletions(-) rename dashboard/dist/assets/{index.1002db9a.css => index.d8eef428.css} (90%) rename dashboard/dist/assets/{index.5a46c6a0.js => index.ea68d73f.js} (52%) delete mode 100644 dashboard/dist/crossword.png delete mode 100644 dashboard/dist/dot-grid.png delete mode 100644 dashboard/dist/pw_maze_white.png diff --git a/dashboard/dist/assets/index.1002db9a.css b/dashboard/dist/assets/index.d8eef428.css similarity index 90% rename from dashboard/dist/assets/index.1002db9a.css rename to dashboard/dist/assets/index.d8eef428.css index 9c56a8b..6721fae 100644 --- a/dashboard/dist/assets/index.1002db9a.css +++ b/dashboard/dist/assets/index.d8eef428.css @@ -1 +1 @@ -.router-link-active{--tw-bg-opacity: 1;background-color:rgb(224 231 255 / var(--tw-bg-opacity));font-weight:700;--tw-text-opacity: 1;color:rgb(49 46 129 / var(--tw-text-opacity))}:root{--popper-theme-background-color: #333333;--popper-theme-background-color-hover: #333333;--popper-theme-text-color: #ffffff;--popper-theme-border-width: 0px;--popper-theme-border-style: solid;--popper-theme-border-radius: 6px;--popper-theme-padding: 4px;--popper-theme-box-shadow: 0 6px 30px -6px rgba(0, 0, 0, .25)}*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input:-ms-input-placeholder,textarea:-ms-input-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after{--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.visible{visibility:visible}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:-webkit-sticky;position:sticky}.right-0{right:0}.top-0{top:0}.-left-1{left:-.25rem}.mb-4{margin-bottom:1rem}.mt-8{margin-top:2rem}.mb-2{margin-bottom:.5rem}.mt-6{margin-top:1.5rem}.mt-4{margin-top:1rem}.mt-3{margin-top:.75rem}.ml-2{margin-left:.5rem}.mr-2{margin-right:.5rem}.-ml-1{margin-left:-.25rem}.mr-3{margin-right:.75rem}.ml-4{margin-left:1rem}.mt-12{margin-top:3rem}.ml-10{margin-left:2.5rem}.mb-6{margin-bottom:1.5rem}.mb-1{margin-bottom:.25rem}.mt-2{margin-top:.5rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-screen{height:100vh}.h-2{height:.5rem}.h-full{height:100%}.h-4{height:1rem}.h-5{height:1.25rem}.min-h-screen{min-height:100vh}.w-\[250px\]{width:250px}.w-\[300px\]{width:300px}.w-full{width:100%}.w-2{width:.5rem}.w-\[50px\]{width:50px}.w-\[100px\]{width:100px}.w-\[150px\]{width:150px}.w-\[200px\]{width:200px}.w-4{width:1rem}.w-5{width:1.25rem}.w-2\/12{width:16.666667%}.max-w-\[calc\(100\%-250px\)\]{max-width:calc(100% - 250px)}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.flex-initial{flex:0 1 auto}.shrink-0{flex-shrink:0}.basis-auto{flex-basis:auto}.basis-\[400px\]{flex-basis:400px}.basis-\[250px\]{flex-basis:250px}.table-fixed{table-layout:fixed}.translate-x-\[300px\]{--tw-translate-x: 300px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@-webkit-keyframes spin{to{transform:rotate(360deg)}}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{-webkit-animation:spin 1s linear infinite;animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-row{flex-direction:row}.items-center{align-items:center}.justify-center{justify-content:center}.gap-4{gap:1rem}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(229 231 235 / var(--tw-divide-opacity))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.text-ellipsis{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.rounded-lg{border-radius:.5rem}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.bg-indigo-100{--tw-bg-opacity: 1;background-color:rgb(224 231 255 / var(--tw-bg-opacity))}.bg-indigo-500{--tw-bg-opacity: 1;background-color:rgb(99 102 241 / var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity))}.bg-green-100{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity))}.bg-blue-200{--tw-bg-opacity: 1;background-color:rgb(191 219 254 / var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity))}.bg-blue-300\/30{background-color:#93c5fd4d}.bg-red-100{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity))}.bg-blue-100{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity))}.bg-red-100\/50{background-color:#fee2e280}.bg-white\/90{background-color:#ffffffe6}.p-6{padding:1.5rem}.p-2{padding:.5rem}.p-4{padding:1rem}.p-8{padding:2rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.px-1{padding-left:.25rem;padding-right:.25rem}.py-0{padding-top:0;padding-bottom:0}.px-4{padding-left:1rem;padding-right:1rem}.pr-6{padding-right:1.5rem}.pt-1{padding-top:.25rem}.pr-12{padding-right:3rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-middle{vertical-align:middle}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-2xl{font-size:1.5rem;line-height:2rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.text-sm{font-size:.875rem;line-height:1.25rem}.font-medium{font-weight:500}.font-light{font-weight:300}.font-semibold{font-weight:600}.font-normal{font-weight:400}.uppercase{text-transform:uppercase}.leading-5{line-height:1.25rem}.leading-none{line-height:1}.tracking-wider{letter-spacing:.05em}.text-indigo-900{--tw-text-opacity: 1;color:rgb(49 46 129 / var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.text-indigo-100{--tw-text-opacity: 1;color:rgb(224 231 255 / var(--tw-text-opacity))}.text-blue-800{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity))}.text-indigo-600{--tw-text-opacity: 1;color:rgb(79 70 229 / var(--tw-text-opacity))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.text-green-800{--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity))}.text-red-800{--tw-text-opacity: 1;color:rgb(153 27 27 / var(--tw-text-opacity))}.text-red-600\/50{color:#dc262680}.text-gray-800{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity))}.text-black\/70{color:#000000b3}.text-black\/20{color:#0003}.opacity-25{opacity:.25}.opacity-75{opacity:.75}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.ring-1{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-white{--tw-ring-opacity: 1;--tw-ring-color: rgb(255 255 255 / var(--tw-ring-opacity))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.\!filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)!important}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,fill,stroke,-webkit-text-decoration-color;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,-webkit-text-decoration-color;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.hover\:scale-\[1\.1\]:hover{--tw-scale-x: 1.1;--tw-scale-y: 1.1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:bg-indigo-100\/10:hover{background-color:#e0e7ff1a}.hover\:text-indigo-900:hover{--tw-text-opacity: 1;color:rgb(49 46 129 / var(--tw-text-opacity))}@media (prefers-color-scheme: dark){.dark\:border-gray-700{--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity))}.dark\:bg-blue-200{--tw-bg-opacity: 1;background-color:rgb(191 219 254 / var(--tw-bg-opacity))}.dark\:text-blue-800{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity))}}@media (min-width: 640px){.sm\:rounded-lg{border-radius:.5rem}}@-webkit-keyframes shake-59cd799c{8%,41%{transform:translate(-10px)}25%,58%{transform:translate(10px)}75%{transform:translate(-5px)}92%{transform:translate(5px)}0%,to{transform:translate(0)}}@keyframes shake-59cd799c{8%,41%{transform:translate(-10px)}25%,58%{transform:translate(10px)}75%{transform:translate(-5px)}92%{transform:translate(5px)}0%,to{transform:translate(0)}}.shake[data-v-59cd799c]{-webkit-animation:shake-59cd799c .3s linear;animation:shake-59cd799c .3s linear}input[type=password][data-v-59cd799c]{font:small-caption;font-size:36px}.task-error{background-color:#fee2e280;color:#dc262680}.task-queued,.task-scheduled{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.task-successful{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity))}.task-failed{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(153 27 27 / var(--tw-text-opacity))}.task-queued{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.task-running{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity))}.task-successful[data-v-35155177]{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity))}.task-failed[data-v-35155177]{--tw-bg-opacity: 1;background-color:rgb(127 29 29 / var(--tw-bg-opacity))}.task-error[data-v-35155177]{--tw-bg-opacity: 1;background-color:rgb(185 28 28 / var(--tw-bg-opacity))}.task-queued[data-v-35155177]{--tw-bg-opacity: 1;background-color:rgb(209 213 219 / var(--tw-bg-opacity))}.task-running[data-v-35155177]{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity))}.no-scroll[data-v-d792a216]::-webkit-scrollbar{display:none} +.router-link-active{--tw-bg-opacity: 1;background-color:rgb(224 231 255 / var(--tw-bg-opacity));font-weight:700;--tw-text-opacity: 1;color:rgb(49 46 129 / var(--tw-text-opacity))}:root{--popper-theme-background-color: #333333;--popper-theme-background-color-hover: #333333;--popper-theme-text-color: #ffffff;--popper-theme-border-width: 0px;--popper-theme-border-style: solid;--popper-theme-border-radius: 6px;--popper-theme-padding: 4px;--popper-theme-box-shadow: 0 6px 30px -6px rgba(0, 0, 0, .25)}*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji"}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,[type=button],[type=reset],[type=submit]{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input:-ms-input-placeholder,textarea:-ms-input-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:before,:after{--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.visible{visibility:visible}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:-webkit-sticky;position:sticky}.right-0{right:0}.top-0{top:0}.-left-1{left:-.25rem}.mb-4{margin-bottom:1rem}.mt-8{margin-top:2rem}.mb-2{margin-bottom:.5rem}.mt-6{margin-top:1.5rem}.mt-4{margin-top:1rem}.mt-3{margin-top:.75rem}.ml-2{margin-left:.5rem}.mr-2{margin-right:.5rem}.-ml-1{margin-left:-.25rem}.mr-3{margin-right:.75rem}.ml-4{margin-left:1rem}.mt-12{margin-top:3rem}.ml-10{margin-left:2.5rem}.mb-6{margin-bottom:1.5rem}.mb-1{margin-bottom:.25rem}.mt-2{margin-top:.5rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.hidden{display:none}.h-screen{height:100vh}.h-2{height:.5rem}.h-full{height:100%}.h-4{height:1rem}.h-5{height:1.25rem}.min-h-screen{min-height:100vh}.w-\[250px\]{width:250px}.w-\[300px\]{width:300px}.w-full{width:100%}.w-2{width:.5rem}.w-\[50px\]{width:50px}.w-\[100px\]{width:100px}.w-\[150px\]{width:150px}.w-\[200px\]{width:200px}.w-4{width:1rem}.w-5{width:1.25rem}.w-2\/12{width:16.666667%}.max-w-\[calc\(100\%-250px\)\]{max-width:calc(100% - 250px)}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.flex-initial{flex:0 1 auto}.shrink-0{flex-shrink:0}.basis-auto{flex-basis:auto}.basis-\[400px\]{flex-basis:400px}.basis-\[250px\]{flex-basis:250px}.table-fixed{table-layout:fixed}.translate-x-\[300px\]{--tw-translate-x: 300px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}@-webkit-keyframes spin{to{transform:rotate(360deg)}}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{-webkit-animation:spin 1s linear infinite;animation:spin 1s linear infinite}.cursor-pointer{cursor:pointer}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.flex-row{flex-direction:row}.items-center{align-items:center}.justify-center{justify-content:center}.gap-4{gap:1rem}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(229 231 235 / var(--tw-divide-opacity))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.text-ellipsis{text-overflow:ellipsis}.whitespace-nowrap{white-space:nowrap}.rounded-lg{border-radius:.5rem}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-l{border-left-width:1px}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity))}.bg-indigo-100{--tw-bg-opacity: 1;background-color:rgb(224 231 255 / var(--tw-bg-opacity))}.bg-indigo-500{--tw-bg-opacity: 1;background-color:rgb(99 102 241 / var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity))}.bg-green-100{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity))}.bg-blue-200{--tw-bg-opacity: 1;background-color:rgb(191 219 254 / var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity))}.bg-blue-300\/30{background-color:#93c5fd4d}.bg-red-100{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity))}.bg-blue-100{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity))}.bg-red-100\/50{background-color:#fee2e280}.bg-white\/90{background-color:#ffffffe6}.p-6{padding:1.5rem}.p-2{padding:.5rem}.p-4{padding:1rem}.p-8{padding:2rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.px-1\.5{padding-left:.375rem;padding-right:.375rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.px-1{padding-left:.25rem;padding-right:.25rem}.py-0{padding-top:0;padding-bottom:0}.px-4{padding-left:1rem;padding-right:1rem}.pr-6{padding-right:1.5rem}.pt-1{padding-top:.25rem}.pr-12{padding-right:3rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-middle{vertical-align:middle}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-2xl{font-size:1.5rem;line-height:2rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.text-sm{font-size:.875rem;line-height:1.25rem}.font-medium{font-weight:500}.font-light{font-weight:300}.font-semibold{font-weight:600}.font-normal{font-weight:400}.uppercase{text-transform:uppercase}.leading-5{line-height:1.25rem}.leading-none{line-height:1}.tracking-wider{letter-spacing:.05em}.text-indigo-900{--tw-text-opacity: 1;color:rgb(49 46 129 / var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity))}.text-indigo-100{--tw-text-opacity: 1;color:rgb(224 231 255 / var(--tw-text-opacity))}.text-blue-800{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity))}.text-indigo-600{--tw-text-opacity: 1;color:rgb(79 70 229 / var(--tw-text-opacity))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity))}.text-green-800{--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity))}.text-red-800{--tw-text-opacity: 1;color:rgb(153 27 27 / var(--tw-text-opacity))}.text-red-600\/50{color:#dc262680}.text-gray-800{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity))}.text-black\/70{color:#000000b3}.text-black\/20{color:#0003}.opacity-25{opacity:.25}.opacity-75{opacity:.75}.shadow-2xl{--tw-shadow: 0 25px 50px -12px rgb(0 0 0 / .25);--tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.outline-none{outline:2px solid transparent;outline-offset:2px}.ring-1{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.ring-white{--tw-ring-opacity: 1;--tw-ring-color: rgb(255 255 255 / var(--tw-ring-opacity))}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.\!filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)!important}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,fill,stroke,-webkit-text-decoration-color;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,-webkit-text-decoration-color;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.hover\:scale-\[1\.1\]:hover{--tw-scale-x: 1.1;--tw-scale-y: 1.1;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skew(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.hover\:bg-indigo-100\/10:hover{background-color:#e0e7ff1a}.hover\:text-indigo-900:hover{--tw-text-opacity: 1;color:rgb(49 46 129 / var(--tw-text-opacity))}@media (prefers-color-scheme: dark){.dark\:border-gray-700{--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity))}.dark\:bg-blue-200{--tw-bg-opacity: 1;background-color:rgb(191 219 254 / var(--tw-bg-opacity))}.dark\:text-blue-800{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity))}}@media (min-width: 640px){.sm\:rounded-lg{border-radius:.5rem}}@-webkit-keyframes shake-59cd799c{8%,41%{transform:translate(-10px)}25%,58%{transform:translate(10px)}75%{transform:translate(-5px)}92%{transform:translate(5px)}0%,to{transform:translate(0)}}@keyframes shake-59cd799c{8%,41%{transform:translate(-10px)}25%,58%{transform:translate(10px)}75%{transform:translate(-5px)}92%{transform:translate(5px)}0%,to{transform:translate(0)}}.shake[data-v-59cd799c]{-webkit-animation:shake-59cd799c .3s linear;animation:shake-59cd799c .3s linear}input[type=password][data-v-59cd799c]{font:small-caption;font-size:36px}.task-error{background-color:#fee2e280;color:#dc262680}.task-queued,.task-scheduled{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.task-running,.task-released{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity))}.task-successful{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity))}.task-failed{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(153 27 27 / var(--tw-text-opacity))}.task-queued{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity))}.task-running{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity))}.task-successful[data-v-35155177]{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity));--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity))}.task-failed[data-v-35155177]{--tw-bg-opacity: 1;background-color:rgb(127 29 29 / var(--tw-bg-opacity))}.task-error[data-v-35155177]{--tw-bg-opacity: 1;background-color:rgb(185 28 28 / var(--tw-bg-opacity))}.task-queued[data-v-35155177]{--tw-bg-opacity: 1;background-color:rgb(209 213 219 / var(--tw-bg-opacity))}.task-running[data-v-35155177]{--tw-bg-opacity: 1;background-color:rgb(59 130 246 / var(--tw-bg-opacity))}.no-scroll[data-v-5df67d4c]::-webkit-scrollbar{display:none} diff --git a/dashboard/dist/assets/index.5a46c6a0.js b/dashboard/dist/assets/index.ea68d73f.js similarity index 52% rename from dashboard/dist/assets/index.5a46c6a0.js rename to dashboard/dist/assets/index.ea68d73f.js index 5a57c92..e61a7bc 100644 --- a/dashboard/dist/assets/index.5a46c6a0.js +++ b/dashboard/dist/assets/index.ea68d73f.js @@ -1 +1 @@ -var W=Object.defineProperty,J=Object.defineProperties;var X=Object.getOwnPropertyDescriptors;var D=Object.getOwnPropertySymbols;var Y=Object.prototype.hasOwnProperty,Z=Object.prototype.propertyIsEnumerable;var E=(s,a,t)=>a in s?W(s,a,{enumerable:!0,configurable:!0,writable:!0,value:t}):s[a]=t,C=(s,a)=>{for(var t in a||(a={}))Y.call(a,t)&&E(s,t,a[t]);if(D)for(var t of D(a))Z.call(a,t)&&E(s,t,a[t]);return s},V=(s,a)=>J(s,X(a));var b=(s,a,t)=>new Promise((r,o)=>{var l=c=>{try{i(t.next(c))}catch(u){o(u)}},n=c=>{try{i(t.throw(c))}catch(u){o(u)}},i=c=>c.done?r(c.value):Promise.resolve(c.value).then(l,n);i((t=t.apply(s,a)).next())});import{r as S,o as d,c as _,a as h,w as x,n as g,F as w,b as y,d as e,e as ee,f as te,u as k,g as m,h as A,i as O,v as se,j as B,p as F,k as N,t as p,l as M,m as oe,q as ae,s as ne,x as q,y as $,z as j,A as le,B as re,C as ie,D as ce}from"./vendor.433de25e.js";const ue=function(){const a=document.createElement("link").relList;if(a&&a.supports&&a.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))r(o);new MutationObserver(o=>{for(const l of o)if(l.type==="childList")for(const n of l.addedNodes)n.tagName==="LINK"&&n.rel==="modulepreload"&&r(n)}).observe(document,{childList:!0,subtree:!0});function t(o){const l={};return o.integrity&&(l.integrity=o.integrity),o.referrerpolicy&&(l.referrerPolicy=o.referrerpolicy),o.crossorigin==="use-credentials"?l.credentials="include":o.crossorigin==="anonymous"?l.credentials="omit":l.credentials="same-origin",l}function r(o){if(o.ep)return;o.ep=!0;const l=t(o);fetch(o.href,l)}};ue();var T=(s,a)=>{const t=s.__vccOpts||s;for(const[r,o]of a)t[r]=o;return t};const de={},pe=y("Dashboard "),_e=y("Recent "),he=y("Queued "),me=y("Failed ");function fe(s,a){var r,o,l,n,i,c,u,f,v;const t=S("router-link");return d(),_(w,null,[h(t,{to:{name:"home"},class:"block p-4 rounded mb-2 cursor-pointer"},{default:x(()=>[pe]),_:1}),h(t,{to:{name:"recent"},class:g(["block p-4 rounded mb-2 cursor-pointer",{"router-link-active":((l=(o=(r=s.$route)==null?void 0:r.matched[0])==null?void 0:o.meta)==null?void 0:l.route)==="recent"}])},{default:x(()=>[_e]),_:1},8,["class"]),h(t,{to:{name:"queued"},class:g(["block p-4 rounded mb-2 cursor-pointer",{"router-link-active":((c=(i=(n=s.$route)==null?void 0:n.matched[0])==null?void 0:i.meta)==null?void 0:c.route)==="queued"}])},{default:x(()=>[he]),_:1},8,["class"]),h(t,{to:{name:"failed"},class:g(["block p-4 rounded mb-2",{"router-link-active":((v=(f=(u=s.$route)==null?void 0:u.matched[0])==null?void 0:f.meta)==null?void 0:v.route)==="failed"}])},{default:x(()=>[me]),_:1},8,["class"])],64)}var xe=T(de,[["render",fe]]);const ve={class:"flex"},ge={class:"basis-auto w-[250px] shrink-0 bg-white p-6 min-h-screen"},ye={class:"flex-1 max-w-[calc(100%-250px)] p-6"},be={setup(s){return(a,t)=>{const r=S("router-view");return d(),_("div",ve,[e("aside",ge,[h(xe)]),e("div",ye,[h(r)])])}}};function I(){return b(this,arguments,function*({endpoint:s,router:a,body:t=null,method:r="GET",login:o=!1}={}){const l=yield fetch(`/cloud-tasks-api/${s}`,V(C({method:r},t?{body:t}:{}),{headers:C({},o?{}:{Authorization:`Bearer ${localStorage.getItem("cloud-tasks-token")}`})}));return l.status===403&&!o&&(localStorage.removeItem("cloud-tasks-token"),a.push({name:"login"})),o?yield l.text():yield l.json()})}function L(r){return b(this,arguments,function*(s,a={},t){let o=!1;const l=function(c){return b(this,null,function*(){if(o)return;const u=new URL(window.location.href),f=new URLSearchParams(u.search);for(const[v,G]of Object.entries(a))f.append(v,G);o=!0,c.value=yield I({endpoint:`tasks?${f.toString()}`,router:t}),o=!1})};l(s);let n=setInterval(()=>l(s),3e3);ee(function(){setTimeout(()=>l(s))});const i=function(){document.visibilityState==="visible"?(l(s),clearInterval(n),n=setInterval(()=>l(s),3e3)):document.visibilityState==="hidden"&&clearInterval(n)};document.addEventListener("visibilitychange",i),te(()=>{clearInterval(n),document.removeEventListener("visibilitychange",i),o=!1})})}const z=s=>(F("data-v-59cd799c"),s=s(),N(),s),we={class:"block w-full h-full flex items-center justify-center"},ke=z(()=>e("h3",{class:"text-4xl"},"This application is password protected.",-1)),$e=["onKeyup","disabled"],Ce=z(()=>e("div",{class:"text-center mt-6 text-xl"},[y(" Press "),e("span",{class:"bg-blue-200 py-1 px-2 ml-2 mr-2 rounded text-blue-800"},"Enter"),y(" to log in. ")],-1)),qe={setup(s){const a=k(),t=m(null),r=m(""),o=m(""),l=m(!1),n=m(!1);A(()=>{t.value.focus()});function i(){return b(this,null,function*(){if(r.value===""||r.value===o.value)return;o.value=r.value;const c=new FormData;c.append("password",r.value),n.value=!0;const u=yield I({endpoint:"login",method:"POST",body:c,login:!0});n.value=!1,u?(localStorage.setItem("cloud-tasks-token",u),a.push({name:"home"})):(l.value=!0,setTimeout(()=>{l.value=!1},820),setTimeout(()=>t.value.focus(),50))})}return(c,u)=>(d(),_("div",we,[e("div",null,[ke,O(e("input",{type:"password",class:g(["w-full p-2 px-6 text-2xl font-light mt-8 text-center outline-none shadow rounded-full",{shake:l.value}]),onKeyup:B(i,["enter"]),"onUpdate:modelValue":u[0]||(u[0]=f=>r.value=f),disabled:n.value,ref_key:"inputRef",ref:t},null,42,$e),[[se,r.value]]),Ce])]))}};var Te=T(qe,[["__scopeId","data-v-59cd799c"]]);const Se=e("h3",{class:"text-3xl mb-4"},"All tasks",-1),Ae={class:"grid grid-cols-3 gap-4"},Ie=["textContent"],Le=e("span",{class:"text-gray-600"},"this minute",-1),Re=["textContent"],Pe=e("span",{class:"text-gray-600"},"this hour",-1),Ue=["textContent"],De=e("span",{class:"text-gray-600"},"today",-1),Ee=e("h3",{class:"text-3xl mb-4 mt-8"},"Failed tasks",-1),Ve={class:"grid grid-cols-3 gap-4"},Oe=["textContent"],Be=e("span",{class:"text-gray-600"},"this minute",-1),Fe=["textContent"],Ne=e("span",{class:"text-gray-600"},"this hour",-1),Me=["textContent"],je=e("span",{class:"text-gray-600"},"today",-1),ze={setup(s){const a=k(),t=m({recent:{this_minute:"...",this_hour:"...",today:"..."},failed:{this_minute:"...",this_hour:"...",today:"..."}});return A(()=>b(this,null,function*(){t.value=yield I({endpoint:"dashboard",router:a})})),(r,o)=>{const l=S("router-link");return d(),_(w,null,[Se,e("div",Ae,[h(l,{to:{name:"recent",query:{time:`${new Date().getUTCHours()}:${new Date().getUTCMinutes()}`}},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,i;return[e("span",{class:"block text-4xl",textContent:p((i=(n=t.value)==null?void 0:n.recent)==null?void 0:i.this_minute)},null,8,Ie),Le]}),_:1},8,["to"]),h(l,{to:{name:"recent",query:{hour:new Date().getUTCHours()}},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,i;return[e("span",{class:"block text-4xl",textContent:p((i=(n=t.value)==null?void 0:n.recent)==null?void 0:i.this_hour)},null,8,Re),Pe]}),_:1},8,["to"]),h(l,{to:{name:"recent"},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,i;return[e("span",{class:"block text-4xl",textContent:p((i=(n=t.value)==null?void 0:n.recent)==null?void 0:i.this_day)},null,8,Ue),De]}),_:1})]),Ee,e("div",Ve,[h(l,{to:{name:"failed",query:{time:`${new Date().getUTCHours()}:${new Date().getUTCMinutes()}`}},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,i;return[e("span",{class:"block text-4xl",textContent:p((i=(n=t.value)==null?void 0:n.failed)==null?void 0:i.this_minute)},null,8,Oe),Be]}),_:1},8,["to"]),h(l,{to:{name:"failed",query:{hour:new Date().getUTCHours()}},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,i;return[e("span",{class:"block text-4xl",textContent:p((i=(n=t.value)==null?void 0:n.failed)==null?void 0:i.this_hour)},null,8,Fe),Ne]}),_:1},8,["to"]),h(l,{to:{name:"failed"},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,i;return[e("span",{class:"block text-4xl",textContent:p((i=(n=t.value)==null?void 0:n.failed)==null?void 0:i.this_day)},null,8,Me),je]}),_:1})])],64)}}};const H={props:{status:String,classes:{type:Array,default:[]}},setup(s){function a(t){return t.charAt(0).toUpperCase()+t.slice(1)}return(t,r)=>(d(),_("span",{class:g(["px-2 inline-flex text-xs leading-5 font-semibold rounded-full",[`task-${s.status}`,...s.classes]])},p(a(s.status)),3))}},He={},Ke=e("tr",null,[e("td",{colspan:"7",class:"px-6 py-4 bg-white"},[e("svg",{class:"animate-spin -ml-1 mr-3 h-5 w-5 text-white",xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24"},[e("circle",{class:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"indigo","stroke-width":"4"}),e("path",{class:"opacity-75",fill:"indigo",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"})])])],-1),Qe=[Ke];function Ge(s,a){return d(),_("tbody",null,Qe)}var We=T(He,[["render",Ge]]);const Je=e("label",{for:"queue",class:"block mb-2 font-medium"},"Queue",-1),Xe=["onKeyup"],Ye=e("label",{for:"status",class:"block mb-2 mt-6 font-medium"},"Status",-1),Ze=ae('',6),et=[Ze],tt={props:{focus:String},setup(s){const a=s,t=k(),r=M(),o=m(!1),l=m(null),n=m(null);function i(){t.push({name:r.name,query:C(C({},l.value.value?{queue:l.value.value}:{}),n.value?{status:n.value}:{})})}function c(u){u===""&&i()}return A(()=>{setTimeout(()=>o.value=!0),a.focus==="queue"&&l.value.focus()}),(u,f)=>(d(),_("div",{class:g(["w-[300px] fixed transition-transform right-0 top-0 p-6 px-6 shadow-2xl h-screen bg-white",{"translate-x-[300px]":o.value===!1}])},[Je,e("input",{type:"text",name:"queue",id:"queue",ref_key:"queue",ref:l,class:"bg-white py-2 px-3 w-full rounded border",onKeyup:[B(i,["enter"]),f[0]||(f[0]=v=>c(v.target.value))]},null,40,Xe),Ye,O(e("select",{name:"status",id:"status","onUpdate:modelValue":f[1]||(f[1]=v=>n.value=v),class:"bg-white py-2 px-3 w-full rounded border"},et,512),[[oe,n.value]]),e("button",{class:"bg-indigo-500 w-full mt-4 text-indigo-100 rounded py-2",onClick:i}," Apply Filter (or Press Enter) ")],2))}};const st={class:"text-4xl mb-2"},ot={class:"text-lg"},at={class:"flex flex-row mt-6"},nt={class:"flex-1"},lt={class:"align-middle"},rt={class:"shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"},it={class:"table-fixed divide-y divide-gray-200 w-full"},ct={class:"bg-gray-50"},ut=e("th",{scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-[50px]"}," # ",-1),dt=e("th",{scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider max-w-xl w-[300px]"}," Name ",-1),pt=e("th",{scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-[100px]"}," Status ",-1),_t=e("th",{scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-[150px] text-center"}," Attempts ",-1),ht=e("th",{scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-[200px]"}," Created ",-1),mt={scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"},ft=y(" Queue "),xt={class:"inline relative"},vt=e("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"},null,-1),gt=[vt],yt=e("th",{scope:"col",class:"relative px-6 py-3"},[e("span",{class:"sr-only"},"Edit")],-1),bt={key:1},wt=e("tr",null,[e("td",{colspan:"7",class:"px-6 py-4 bg-white"},"No results.")],-1),kt=[wt],$t={class:"bg-white divide-y divide-gray-200"},Ct=["onClick"],qt={class:"px-6 py-4 whitespace-nowrap text-sm text-gray-900"},Tt={class:"px-6 py-4 whitespace-nowrap text-ellipsis text-sm text-gray-900"},St={class:"px-6 py-4 whitespace-nowrap"},At={class:"px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-center"},It={class:"px-6 py-4 whitespace-nowrap text-sm text-gray-500"},Lt={class:"px-6 py-4 whitespace-nowrap text-sm text-gray-500"},Rt=e("td",{class:"px-6 py-4 whitespace-nowrap text-right text-sm font-medium"},[e("a",{href:"#",class:"text-indigo-600 hover:text-indigo-900"},"View")],-1),R={props:{title:String,description:String,tasks:Array},setup(s){const a=s,t=m([]),r=m([]),o=m({visible:!1,focus:null});function l(n){t.value.push(n.id),setTimeout(()=>{t.value.splice(t.value.indexOf(n.id),1)},1e3)}return ne(()=>a.tasks,(n,i)=>{var c;if(!!i){r.value=[],i.map((u,f)=>{r[u.id]=f});for(const u of n)(r[u.id]===void 0||((c=i[r[u.id]])==null?void 0:c.status)!==u.status)&&l(u)}}),(n,i)=>(d(),_(w,null,[e("h1",st,p(s.title),1),e("p",ot,p(s.description),1),e("div",at,[e("div",nt,[e("div",lt,[e("div",rt,[e("table",it,[e("thead",ct,[e("tr",null,[ut,dt,pt,_t,ht,e("th",mt,[ft,e("div",xt,[(d(),_("svg",{xmlns:"http://www.w3.org/2000/svg",class:"h-4 w-4 inline transition-transform hover:scale-[1.1] cursor-pointer",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",onClick:i[0]||(i[0]=()=>{o.value.visible=!o.value.visible,o.value.focus=o.value.visible?"queue":null})},gt))])]),yt])]),s.tasks===null?(d(),q(We,{key:0})):$("",!0),s.tasks&&s.tasks.length===0?(d(),_("tbody",bt,kt)):$("",!0),e("tbody",$t,[(d(!0),_(w,null,j(s.tasks,c=>(d(),_("tr",{class:g(["cursor-pointer hover:bg-indigo-100/10 transition-colors",{"bg-blue-300/30":t.value.includes(c.id)}]),onClick:u=>n.$router.push({name:`${n.$route.name}-task`,params:{uuid:c.uuid}})},[e("td",qt,p(c.id),1),e("td",Tt,p(c.name.substring(0,30))+p(c.name.length>30?"...":""),1),e("td",St,[h(H,{status:c.status},null,8,["status"])]),e("td",At,p(c.attempts),1),e("td",It,p(c.created),1),e("td",Lt,p(c.queue),1),Rt],10,Ct))),256))])])])])])]),o.value.visible?(d(),q(tt,{key:0,visible:o.value.visible,focus:o.value.focus},null,8,["visible","focus"])):$("",!0)],64))}},Pt={props:{tasks:Array},setup(s){const a=m(null),t=k();return L(a,{filter:"recent"},t),(r,o)=>(d(),q(R,{title:"Recent tasks",description:"Tasks that have been added or processed in the queue recently.",tasks:a.value},null,8,["tasks"]))}},Ut={props:{tasks:Array},setup(s){const a=m(null),t=k();return L(a,{status:"queued"},t),(r,o)=>(d(),q(R,{title:"Queued tasks",description:"Tasks that have been added to the queue recently.",tasks:a.value},null,8,["tasks"]))}},Dt={props:{tasks:Array},setup(s){const a=m(null),t=k();return L(a,{filter:"failed"},t),(r,o)=>(d(),q(R,{title:"Failed tasks",description:"Tasks that permanently failed after they have reached their max number of attempts.",tasks:a.value},null,8,["tasks"]))}};const Et={class:"absolute flex items-center justify-center w-2 h-2 bg-gray-200 rounded-full -left-1 ring-1 mt-3 ring-white"},Vt={props:{status:String,classes:{type:Array,default:[]}},setup(s){return(a,t)=>(d(),_("span",Et))}};var Ot=T(Vt,[["__scopeId","data-v-35155177"]]);const P=s=>(F("data-v-d792a216"),s=s(),N(),s),Bt={class:"text-4xl mb-2"},Ft={class:"flex"},Nt={class:"basis-[400px] shrink-0 pr-6 w-2/12"},Mt={class:"flex-initial sticky ml-4 mt-12"},jt={class:"relative border-l border-gray-200 dark:border-gray-700"},zt={class:"text-gray-900"},Ht={key:0,class:"bg-blue-100 text-blue-800 text-xs font-medium mr-2 inline-block mb-1 px-1.5 py-0.5 rounded dark:bg-blue-200 dark:text-blue-800"},Kt={key:0},Qt={class:"bg-gray-200 text-gray-800 text-xs font-medium mr-2 inline-block mb-1 px-1.5 py-0.5 rounded dark:bg-blue-200 dark:text-blue-800"},Gt={class:"block mb-2 mt-2 text-xs text-black/70 font-normal leading-none"},Wt={class:"cursor-default"},Jt={class:"basis-auto overflow-x-auto pr-12"},Xt=P(()=>e("h2",{class:"text-2xl"},"Task Exception",-1)),Yt={class:"text-xs p-8 border border-[#ccc/80] bg-white/90 mt-4 rounded overflow-auto no-scroll"},Zt={key:1,class:"mt-12"},es=P(()=>e("h2",{class:"text-2xl"},"Task Payload",-1)),ts={class:"text-xs p-8 border border-[#ccc/80] bg-white/90 mt-4 rounded overflow-auto no-scroll"},ss=P(()=>e("div",{class:"basis-[250px] shrink-0 px-6"},[e("h2",{class:"text-3xl"},"Actions"),e("button",{class:"bg-gray-200 text-black/20 cursor-not-allowed mt-4 w-full rounded px-4 py-2"}," Retry "),e("span",{class:"text-xs text-gray-800 mt-2 inline-block"},"Retrying tasks is not available yet.")],-1)),os={setup(s){const a=M(),t=k(),r=m({id:null,status:"loading"});A(()=>b(this,null,function*(){r.value=yield I({endpoint:`task/${a.params.uuid}`,router:t})}));const o={scheduled:"Scheduled",queued:"Added to the queue",running:"Running",successful:"Successful",error:"An error occurred",failed:"Failed permanently"};return(l,n)=>{const i=S("Popper");return d(),_(w,null,[e("h1",Bt,"Task #"+p(r.value.id),1),h(H,{status:r.value.status,classes:["text-sm"]},null,8,["status"]),e("div",Ft,[e("div",Nt,[e("div",Mt,[e("ol",jt,[(d(!0),_(w,null,j(r.value.events,(c,u)=>(d(),_("li",{class:g(["ml-10 pt-1 mb-6",[`event-${c.status}`]])},[h(Ot,{status:c.status},null,8,["status"]),e("h3",zt,[y(p(o[c.status]||c.status)+" ",1),e("div",null,[c.queue?(d(),_("span",Ht,p(r.value.queue),1)):$("",!0)]),c.scheduled_at?(d(),_("div",Kt,[e("span",Qt," Scheduled: "+p(c.scheduled_at)+" (UTC) ",1)])):$("",!0)]),h(i,{content:c.datetime,hover:!0,arrow:!0,placement:"right"},{default:x(()=>[e("time",Gt,[e("span",Wt,p(c.diff),1)])]),_:2},1032,["content"])],2))),256))])])]),e("div",Jt,[r.value.exception?(d(),_(w,{key:0},[Xt,e("pre",Yt,p(r.value.exception),1)],64)):$("",!0),r.value.payload?(d(),_("div",Zt,[es,e("pre",ts,p(r.value.payload),1)])):$("",!0)]),ss])],64)}}};var U=T(os,[["__scopeId","data-v-d792a216"]]);const as=[{name:"home",path:"/",component:ze},{name:"login",path:"/login",component:Te},{name:"recent",path:"/recent",component:Pt,meta:{route:"recent"}},{name:"recent-task",path:"/recent/:uuid",component:U,meta:{route:"recent"}},{name:"queued",path:"/queued",component:Ut,meta:{route:"queued"}},{name:"queued-task",path:"/queued/:uuid",component:U,meta:{route:"queued"}},{name:"failed",path:"/failed",component:Dt,meta:{route:"failed"}},{name:"failed-task",path:"/failed/:uuid",component:U,meta:{route:"failed"}}];let K=null;"CloudTasks"in window&&(K=`/${window.CloudTasks.path}`);const Q=le({history:re(K),routes:as});Q.beforeEach((s,a,t)=>!localStorage.hasOwnProperty("cloud-tasks-token")&&s.name!=="login"?t({name:"login"}):t());ie(be).use(Q).component("Popper",ce).mount("#app"); +var W=Object.defineProperty,J=Object.defineProperties;var X=Object.getOwnPropertyDescriptors;var D=Object.getOwnPropertySymbols;var Y=Object.prototype.hasOwnProperty,Z=Object.prototype.propertyIsEnumerable;var E=(s,a,t)=>a in s?W(s,a,{enumerable:!0,configurable:!0,writable:!0,value:t}):s[a]=t,C=(s,a)=>{for(var t in a||(a={}))Y.call(a,t)&&E(s,t,a[t]);if(D)for(var t of D(a))Z.call(a,t)&&E(s,t,a[t]);return s},V=(s,a)=>J(s,X(a));var w=(s,a,t)=>new Promise((r,o)=>{var l=i=>{try{c(t.next(i))}catch(u){o(u)}},n=i=>{try{c(t.throw(i))}catch(u){o(u)}},c=i=>i.done?r(i.value):Promise.resolve(i.value).then(l,n);c((t=t.apply(s,a)).next())});import{r as S,o as d,c as _,a as h,w as x,n as y,F as k,b as g,d as e,e as ee,f as te,u as $,g as m,h as A,i as O,v as se,j as B,p as F,k as N,t as p,l as M,m as oe,q as ae,s as ne,x as q,y as b,z as j,A as le,B as re,C as ie,D as ce}from"./vendor.433de25e.js";const ue=function(){const a=document.createElement("link").relList;if(a&&a.supports&&a.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))r(o);new MutationObserver(o=>{for(const l of o)if(l.type==="childList")for(const n of l.addedNodes)n.tagName==="LINK"&&n.rel==="modulepreload"&&r(n)}).observe(document,{childList:!0,subtree:!0});function t(o){const l={};return o.integrity&&(l.integrity=o.integrity),o.referrerpolicy&&(l.referrerPolicy=o.referrerpolicy),o.crossorigin==="use-credentials"?l.credentials="include":o.crossorigin==="anonymous"?l.credentials="omit":l.credentials="same-origin",l}function r(o){if(o.ep)return;o.ep=!0;const l=t(o);fetch(o.href,l)}};ue();var T=(s,a)=>{const t=s.__vccOpts||s;for(const[r,o]of a)t[r]=o;return t};const de={},pe=g("Dashboard "),_e=g("Recent "),he=g("Queued "),me=g("Failed ");function fe(s,a){var r,o,l,n,c,i,u,f,v;const t=S("router-link");return d(),_(k,null,[h(t,{to:{name:"home"},class:"block p-4 rounded mb-2 cursor-pointer"},{default:x(()=>[pe]),_:1}),h(t,{to:{name:"recent"},class:y(["block p-4 rounded mb-2 cursor-pointer",{"router-link-active":((l=(o=(r=s.$route)==null?void 0:r.matched[0])==null?void 0:o.meta)==null?void 0:l.route)==="recent"}])},{default:x(()=>[_e]),_:1},8,["class"]),h(t,{to:{name:"queued"},class:y(["block p-4 rounded mb-2 cursor-pointer",{"router-link-active":((i=(c=(n=s.$route)==null?void 0:n.matched[0])==null?void 0:c.meta)==null?void 0:i.route)==="queued"}])},{default:x(()=>[he]),_:1},8,["class"]),h(t,{to:{name:"failed"},class:y(["block p-4 rounded mb-2",{"router-link-active":((v=(f=(u=s.$route)==null?void 0:u.matched[0])==null?void 0:f.meta)==null?void 0:v.route)==="failed"}])},{default:x(()=>[me]),_:1},8,["class"])],64)}var xe=T(de,[["render",fe]]);const ve={class:"flex"},ye={class:"basis-auto w-[250px] shrink-0 bg-white p-6 min-h-screen"},ge={class:"flex-1 max-w-[calc(100%-250px)] p-6"},be={setup(s){return(a,t)=>{const r=S("router-view");return d(),_("div",ve,[e("aside",ye,[h(xe)]),e("div",ge,[h(r)])])}}};function I(){return w(this,arguments,function*({endpoint:s,router:a,body:t=null,method:r="GET",login:o=!1}={}){const l=yield fetch(`/cloud-tasks-api/${s}`,V(C({method:r},t?{body:t}:{}),{headers:C({},o?{}:{Authorization:`Bearer ${localStorage.getItem("cloud-tasks-token")}`})}));return l.status===403&&!o&&(localStorage.removeItem("cloud-tasks-token"),a.push({name:"login"})),o?yield l.text():yield l.json()})}function R(r){return w(this,arguments,function*(s,a={},t){let o=!1;const l=function(i){return w(this,null,function*(){if(o)return;const u=new URL(window.location.href),f=new URLSearchParams(u.search);for(const[v,G]of Object.entries(a))f.append(v,G);o=!0,i.value=yield I({endpoint:`tasks?${f.toString()}`,router:t}),o=!1})};l(s);let n=setInterval(()=>l(s),3e3);ee(function(){setTimeout(()=>l(s))});const c=function(){document.visibilityState==="visible"?(l(s),clearInterval(n),n=setInterval(()=>l(s),3e3)):document.visibilityState==="hidden"&&clearInterval(n)};document.addEventListener("visibilitychange",c),te(()=>{clearInterval(n),document.removeEventListener("visibilitychange",c),o=!1})})}const z=s=>(F("data-v-59cd799c"),s=s(),N(),s),we={class:"block w-full h-full flex items-center justify-center"},ke=z(()=>e("h3",{class:"text-4xl"},"This application is password protected.",-1)),$e=["onKeyup","disabled"],Ce=z(()=>e("div",{class:"text-center mt-6 text-xl"},[g(" Press "),e("span",{class:"bg-blue-200 py-1 px-2 ml-2 mr-2 rounded text-blue-800"},"Enter"),g(" to log in. ")],-1)),qe={setup(s){const a=$(),t=m(null),r=m(""),o=m(""),l=m(!1),n=m(!1);A(()=>{t.value.focus()});function c(){return w(this,null,function*(){if(r.value===""||r.value===o.value)return;o.value=r.value;const i=new FormData;i.append("password",r.value),n.value=!0;const u=yield I({endpoint:"login",method:"POST",body:i,login:!0});n.value=!1,u?(localStorage.setItem("cloud-tasks-token",u),a.push({name:"home"})):(l.value=!0,setTimeout(()=>{l.value=!1},820),setTimeout(()=>t.value.focus(),50))})}return(i,u)=>(d(),_("div",we,[e("div",null,[ke,O(e("input",{type:"password",class:y(["w-full p-2 px-6 text-2xl font-light mt-8 text-center outline-none shadow rounded-full",{shake:l.value}]),onKeyup:B(c,["enter"]),"onUpdate:modelValue":u[0]||(u[0]=f=>r.value=f),disabled:n.value,ref_key:"inputRef",ref:t},null,42,$e),[[se,r.value]]),Ce])]))}};var Te=T(qe,[["__scopeId","data-v-59cd799c"]]);const Se=e("h3",{class:"text-3xl mb-4"},"All tasks",-1),Ae={class:"grid grid-cols-3 gap-4"},Ie=["textContent"],Re=e("span",{class:"text-gray-600"},"this minute",-1),Le=["textContent"],Pe=e("span",{class:"text-gray-600"},"this hour",-1),Ue=["textContent"],De=e("span",{class:"text-gray-600"},"today",-1),Ee=e("h3",{class:"text-3xl mb-4 mt-8"},"Failed tasks",-1),Ve={class:"grid grid-cols-3 gap-4"},Oe=["textContent"],Be=e("span",{class:"text-gray-600"},"this minute",-1),Fe=["textContent"],Ne=e("span",{class:"text-gray-600"},"this hour",-1),Me=["textContent"],je=e("span",{class:"text-gray-600"},"today",-1),ze={setup(s){const a=$(),t=m({recent:{this_minute:"...",this_hour:"...",today:"..."},failed:{this_minute:"...",this_hour:"...",today:"..."}});return A(()=>w(this,null,function*(){t.value=yield I({endpoint:"dashboard",router:a})})),(r,o)=>{const l=S("router-link");return d(),_(k,null,[Se,e("div",Ae,[h(l,{to:{name:"recent",query:{time:`${new Date().getUTCHours()}:${new Date().getUTCMinutes()}`}},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,c;return[e("span",{class:"block text-4xl",textContent:p((c=(n=t.value)==null?void 0:n.recent)==null?void 0:c.this_minute)},null,8,Ie),Re]}),_:1},8,["to"]),h(l,{to:{name:"recent",query:{hour:new Date().getUTCHours()}},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,c;return[e("span",{class:"block text-4xl",textContent:p((c=(n=t.value)==null?void 0:n.recent)==null?void 0:c.this_hour)},null,8,Le),Pe]}),_:1},8,["to"]),h(l,{to:{name:"recent"},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,c;return[e("span",{class:"block text-4xl",textContent:p((c=(n=t.value)==null?void 0:n.recent)==null?void 0:c.this_day)},null,8,Ue),De]}),_:1})]),Ee,e("div",Ve,[h(l,{to:{name:"failed",query:{time:`${new Date().getUTCHours()}:${new Date().getUTCMinutes()}`}},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,c;return[e("span",{class:"block text-4xl",textContent:p((c=(n=t.value)==null?void 0:n.failed)==null?void 0:c.this_minute)},null,8,Oe),Be]}),_:1},8,["to"]),h(l,{to:{name:"failed",query:{hour:new Date().getUTCHours()}},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,c;return[e("span",{class:"block text-4xl",textContent:p((c=(n=t.value)==null?void 0:n.failed)==null?void 0:c.this_hour)},null,8,Fe),Ne]}),_:1},8,["to"]),h(l,{to:{name:"failed"},class:"bg-white rounded-lg p-6"},{default:x(()=>{var n,c;return[e("span",{class:"block text-4xl",textContent:p((c=(n=t.value)==null?void 0:n.failed)==null?void 0:c.this_day)},null,8,Me),je]}),_:1})])],64)}}};const H={props:{status:String,classes:{type:Array,default:[]}},setup(s){function a(t){return t.charAt(0).toUpperCase()+t.slice(1)}return(t,r)=>(d(),_("span",{class:y(["px-2 inline-flex text-xs leading-5 font-semibold rounded-full",[`task-${s.status}`,...s.classes]])},p(a(s.status)),3))}},He={},Ke=e("tr",null,[e("td",{colspan:"7",class:"px-6 py-4 bg-white"},[e("svg",{class:"animate-spin -ml-1 mr-3 h-5 w-5 text-white",xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24"},[e("circle",{class:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"indigo","stroke-width":"4"}),e("path",{class:"opacity-75",fill:"indigo",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"})])])],-1),Qe=[Ke];function Ge(s,a){return d(),_("tbody",null,Qe)}var We=T(He,[["render",Ge]]);const Je=e("label",{for:"queue",class:"block mb-2 font-medium"},"Queue",-1),Xe=["onKeyup"],Ye=e("label",{for:"status",class:"block mb-2 mt-6 font-medium"},"Status",-1),Ze=ae('',6),et=[Ze],tt={props:{focus:String},setup(s){const a=s,t=$(),r=M(),o=m(!1),l=m(null),n=m(null);function c(){t.push({name:r.name,query:C(C({},l.value.value?{queue:l.value.value}:{}),n.value?{status:n.value}:{})})}function i(u){u===""&&c()}return A(()=>{setTimeout(()=>o.value=!0),a.focus==="queue"&&l.value.focus()}),(u,f)=>(d(),_("div",{class:y(["w-[300px] fixed transition-transform right-0 top-0 p-6 px-6 shadow-2xl h-screen bg-white",{"translate-x-[300px]":o.value===!1}])},[Je,e("input",{type:"text",name:"queue",id:"queue",ref_key:"queue",ref:l,class:"bg-white py-2 px-3 w-full rounded border",onKeyup:[B(c,["enter"]),f[0]||(f[0]=v=>i(v.target.value))]},null,40,Xe),Ye,O(e("select",{name:"status",id:"status","onUpdate:modelValue":f[1]||(f[1]=v=>n.value=v),class:"bg-white py-2 px-3 w-full rounded border"},et,512),[[oe,n.value]]),e("button",{class:"bg-indigo-500 w-full mt-4 text-indigo-100 rounded py-2",onClick:c}," Apply Filter (or Press Enter) ")],2))}};const st={class:"text-4xl mb-2"},ot={class:"text-lg"},at={class:"flex flex-row mt-6"},nt={class:"flex-1"},lt={class:"align-middle"},rt={class:"shadow overflow-hidden border-b border-gray-200 sm:rounded-lg"},it={class:"table-fixed divide-y divide-gray-200 w-full"},ct={class:"bg-gray-50"},ut=e("th",{scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-[50px]"}," # ",-1),dt=e("th",{scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider max-w-xl w-[300px]"}," Name ",-1),pt=e("th",{scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-[100px]"}," Status ",-1),_t=e("th",{scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-[150px] text-center"}," Attempts ",-1),ht=e("th",{scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider w-[200px]"}," Created ",-1),mt={scope:"col",class:"px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"},ft=g(" Queue "),xt={class:"inline relative"},vt=e("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"},null,-1),yt=[vt],gt=e("th",{scope:"col",class:"relative px-6 py-3"},[e("span",{class:"sr-only"},"Edit")],-1),bt={key:1},wt=e("tr",null,[e("td",{colspan:"7",class:"px-6 py-4 bg-white"},"No results.")],-1),kt=[wt],$t={class:"bg-white divide-y divide-gray-200"},Ct=["onClick"],qt={class:"px-6 py-4 whitespace-nowrap text-sm text-gray-900"},Tt={class:"px-6 py-4 whitespace-nowrap text-ellipsis text-sm text-gray-900"},St={class:"px-6 py-4 whitespace-nowrap"},At={class:"px-6 py-4 whitespace-nowrap text-sm text-gray-500 text-center"},It={class:"px-6 py-4 whitespace-nowrap text-sm text-gray-500"},Rt={class:"px-6 py-4 whitespace-nowrap text-sm text-gray-500"},Lt=e("td",{class:"px-6 py-4 whitespace-nowrap text-right text-sm font-medium"},[e("a",{href:"#",class:"text-indigo-600 hover:text-indigo-900"},"View")],-1),L={props:{title:String,description:String,tasks:Array},setup(s){const a=s,t=m([]),r=m([]),o=m({visible:!1,focus:null});function l(n){t.value.push(n.id),setTimeout(()=>{t.value.splice(t.value.indexOf(n.id),1)},1e3)}return ne(()=>a.tasks,(n,c)=>{var i;if(!!c){r.value=[],c.map((u,f)=>{r[u.id]=f});for(const u of n)(r[u.id]===void 0||((i=c[r[u.id]])==null?void 0:i.status)!==u.status)&&l(u)}}),(n,c)=>(d(),_(k,null,[e("h1",st,p(s.title),1),e("p",ot,p(s.description),1),e("div",at,[e("div",nt,[e("div",lt,[e("div",rt,[e("table",it,[e("thead",ct,[e("tr",null,[ut,dt,pt,_t,ht,e("th",mt,[ft,e("div",xt,[(d(),_("svg",{xmlns:"http://www.w3.org/2000/svg",class:"h-4 w-4 inline transition-transform hover:scale-[1.1] cursor-pointer",fill:"none",viewBox:"0 0 24 24",stroke:"currentColor",onClick:c[0]||(c[0]=()=>{o.value.visible=!o.value.visible,o.value.focus=o.value.visible?"queue":null})},yt))])]),gt])]),s.tasks===null?(d(),q(We,{key:0})):b("",!0),s.tasks&&s.tasks.length===0?(d(),_("tbody",bt,kt)):b("",!0),e("tbody",$t,[(d(!0),_(k,null,j(s.tasks,i=>(d(),_("tr",{class:y(["cursor-pointer hover:bg-indigo-100/10 transition-colors",{"bg-blue-300/30":t.value.includes(i.id)}]),onClick:u=>n.$router.push({name:`${n.$route.name}-task`,params:{uuid:i.uuid}})},[e("td",qt,p(i.id),1),e("td",Tt,p(i.name.substring(0,30))+p(i.name.length>30?"...":""),1),e("td",St,[h(H,{status:i.status},null,8,["status"])]),e("td",At,p(i.attempts),1),e("td",It,p(i.created),1),e("td",Rt,p(i.queue),1),Lt],10,Ct))),256))])])])])])]),o.value.visible?(d(),q(tt,{key:0,visible:o.value.visible,focus:o.value.focus},null,8,["visible","focus"])):b("",!0)],64))}},Pt={props:{tasks:Array},setup(s){const a=m(null),t=$();return R(a,{filter:"recent"},t),(r,o)=>(d(),q(L,{title:"Recent tasks",description:"Tasks that have been added or processed in the queue recently.",tasks:a.value},null,8,["tasks"]))}},Ut={props:{tasks:Array},setup(s){const a=m(null),t=$();return R(a,{status:"queued"},t),(r,o)=>(d(),q(L,{title:"Queued tasks",description:"Tasks that have been added to the queue recently.",tasks:a.value},null,8,["tasks"]))}},Dt={props:{tasks:Array},setup(s){const a=m(null),t=$();return R(a,{filter:"failed"},t),(r,o)=>(d(),q(L,{title:"Failed tasks",description:"Tasks that permanently failed after they have reached their max number of attempts.",tasks:a.value},null,8,["tasks"]))}};const Et={class:"absolute flex items-center justify-center w-2 h-2 bg-gray-200 rounded-full -left-1 ring-1 mt-3 ring-white"},Vt={props:{status:String,classes:{type:Array,default:[]}},setup(s){return(a,t)=>(d(),_("span",Et))}};var Ot=T(Vt,[["__scopeId","data-v-35155177"]]);const P=s=>(F("data-v-5df67d4c"),s=s(),N(),s),Bt={class:"text-4xl mb-2"},Ft={class:"flex"},Nt={class:"basis-[400px] shrink-0 pr-6 w-2/12"},Mt={class:"flex-initial sticky ml-4 mt-12"},jt={class:"relative border-l border-gray-200 dark:border-gray-700"},zt={class:"text-gray-900"},Ht={key:0,class:"bg-blue-100 text-blue-800 text-xs font-medium mr-2 inline-block mb-1 px-1.5 py-0.5 rounded dark:bg-blue-200 dark:text-blue-800"},Kt={key:0},Qt={class:"bg-gray-200 text-gray-800 text-xs font-medium mr-2 inline-block mb-1 px-1.5 py-0.5 rounded dark:bg-blue-200 dark:text-blue-800"},Gt={key:1},Wt={class:"bg-gray-200 text-gray-800 text-xs font-medium mr-2 inline-block mb-1 px-1.5 py-0.5 rounded dark:bg-blue-200 dark:text-blue-800"},Jt={class:"block mb-2 mt-2 text-xs text-black/70 font-normal leading-none"},Xt={class:"cursor-default"},Yt={class:"basis-auto overflow-x-auto pr-12"},Zt=P(()=>e("h2",{class:"text-2xl"},"Task Exception",-1)),es={class:"text-xs p-8 border border-[#ccc/80] bg-white/90 mt-4 rounded overflow-auto no-scroll"},ts={key:1,class:"mt-12"},ss=P(()=>e("h2",{class:"text-2xl"},"Task Payload",-1)),os={class:"text-xs p-8 border border-[#ccc/80] bg-white/90 mt-4 rounded overflow-auto no-scroll"},as=P(()=>e("div",{class:"basis-[250px] shrink-0 px-6"},[e("h2",{class:"text-3xl"},"Actions"),e("button",{class:"bg-gray-200 text-black/20 cursor-not-allowed mt-4 w-full rounded px-4 py-2"}," Retry "),e("span",{class:"text-xs text-gray-800 mt-2 inline-block"},"Retrying tasks is not available yet.")],-1)),ns={setup(s){const a=M(),t=$(),r=m({id:null,status:"loading"});A(()=>w(this,null,function*(){r.value=yield I({endpoint:`task/${a.params.uuid}`,router:t})}));const o={scheduled:"Scheduled",queued:"Added to the queue",running:"Running",successful:"Successful",error:"An error occurred",failed:"Failed permanently",released:"Released"};return(l,n)=>{const c=S("Popper");return d(),_(k,null,[e("h1",Bt,"Task #"+p(r.value.id),1),h(H,{status:r.value.status,classes:["text-sm"]},null,8,["status"]),e("div",Ft,[e("div",Nt,[e("div",Mt,[e("ol",jt,[(d(!0),_(k,null,j(r.value.events,(i,u)=>(d(),_("li",{class:y(["ml-10 pt-1 mb-6",[`event-${i.status}`]])},[h(Ot,{status:i.status},null,8,["status"]),e("h3",zt,[g(p(o[i.status]||i.status)+" ",1),e("div",null,[i.queue?(d(),_("span",Ht,p(r.value.queue),1)):b("",!0)]),i.scheduled_at?(d(),_("div",Kt,[e("span",Qt," Scheduled: "+p(i.scheduled_at)+" (UTC) ",1)])):b("",!0),i.delay?(d(),_("div",Gt,[e("span",Wt," Delay: "+p(i.delay)+" seconds ",1)])):b("",!0)]),h(c,{content:i.datetime,hover:!0,arrow:!0,placement:"right"},{default:x(()=>[e("time",Jt,[e("span",Xt,p(i.diff),1)])]),_:2},1032,["content"])],2))),256))])])]),e("div",Yt,[r.value.exception?(d(),_(k,{key:0},[Zt,e("pre",es,p(r.value.exception),1)],64)):b("",!0),r.value.payload?(d(),_("div",ts,[ss,e("pre",os,p(r.value.payload),1)])):b("",!0)]),as])],64)}}};var U=T(ns,[["__scopeId","data-v-5df67d4c"]]);const ls=[{name:"home",path:"/",component:ze},{name:"login",path:"/login",component:Te},{name:"recent",path:"/recent",component:Pt,meta:{route:"recent"}},{name:"recent-task",path:"/recent/:uuid",component:U,meta:{route:"recent"}},{name:"queued",path:"/queued",component:Ut,meta:{route:"queued"}},{name:"queued-task",path:"/queued/:uuid",component:U,meta:{route:"queued"}},{name:"failed",path:"/failed",component:Dt,meta:{route:"failed"}},{name:"failed-task",path:"/failed/:uuid",component:U,meta:{route:"failed"}}];let K=null;"CloudTasks"in window&&(K=`/${window.CloudTasks.path}`);const Q=le({history:re(K),routes:ls});Q.beforeEach((s,a,t)=>!localStorage.hasOwnProperty("cloud-tasks-token")&&s.name!=="login"?t({name:"login"}):t());ie(be).use(Q).component("Popper",ce).mount("#app"); diff --git a/dashboard/dist/crossword.png b/dashboard/dist/crossword.png deleted file mode 100644 index 2f9f1ad08473dcf728044895fd7809749e38eb9e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43694 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 Bg`WTb diff --git a/dashboard/dist/index.html b/dashboard/dist/index.html index 5b5fcee..440a7b8 100644 --- a/dashboard/dist/index.html +++ b/dashboard/dist/index.html @@ -5,9 +5,9 @@ Vite App - + - +
diff --git a/dashboard/dist/manifest.json b/dashboard/dist/manifest.json index fedb792..53f0594 100644 --- a/dashboard/dist/manifest.json +++ b/dashboard/dist/manifest.json @@ -1,13 +1,13 @@ { "index.html": { - "file": "assets/index.5a46c6a0.js", + "file": "assets/index.ea68d73f.js", "src": "index.html", "isEntry": true, "imports": [ "_vendor.433de25e.js" ], "css": [ - "assets/index.1002db9a.css" + "assets/index.d8eef428.css" ] }, "_vendor.433de25e.js": { diff --git a/dashboard/dist/pw_maze_white.png b/dashboard/dist/pw_maze_white.png deleted file mode 100644 index 66464831c0c4389471dfb0ba94981005f77d12a6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 600 zcmV-e0;m0nP)gws~>GJaO^z`)g_4W4l_Q@I-8vpR-ww`TUoSA}Vkbc?jsbu<&5t1W$|wV}QxLbi{VYj4Y( z`Wq08&8%9Lq<*SnC&26reul>-j;)mzOK*usBd~z4+s{{H3 zD>h_8R#wH+sWAAKse2^!mr1pY=7g9QZ-Rhz*OSi(^SM5HLr070t0mQ;oNx2Jiqsml z|Cf-?aF&&SgS_)psZ(#@&N&dUXXJ9$VWGMczStPj!hpyHPDA< zl*~{c(JqZ;n=T7GWgTAh1l@1gYVX@0`RFl`?7lTBs@~GXaAZeLm&shrznR>m^kjxt7JFuzI^0K7(u6l$&=SMg64tmFQP(razaoluGO^R+;~))Jg0fxzQ-%K4f?}b` z?hV(Yr_ib$+*z^5!s##cv)T(StdAMgjy8k%GxG53^2yf<;{FDUu5LEynSYw#AlO}f mzkdv|erYfIv3Tu9Ab$btOBd6f7(Ds_0000