├── .php-cs-fixer.php ├── LICENCE.md ├── README.md ├── composer.json ├── config └── laravel-job-status.php ├── database ├── factories │ ├── JobBatchFactory.php │ ├── JobExceptionFactory.php │ ├── JobMessageFactory.php │ ├── JobSignalFactory.php │ ├── JobStatusFactory.php │ ├── JobStatusStatusFactory.php │ ├── JobStatusTagFactory.php │ └── JobStatusUserFactory.php └── migrations │ ├── 2022_07_24_001000_create_job_statuses_table.php │ ├── 2022_07_24_001100_create_job_messages_table.php │ ├── 2022_07_24_001200_create_job_signals_table.php │ ├── 2022_07_24_001300_create_job_status_tags_table.php │ ├── 2022_07_24_001400_create_job_status_statuses_table.php │ ├── 2023_01_25_205630_create_job_status_exceptions_table.php │ ├── 2023_01_25_235630_link_job_status_exceptions_table_to_job_status_table.php │ ├── 2023_01_26_205630_create_job_status_batches_table.php │ ├── 2023_01_26_235630_link_job_status_batches_table_to_job_status_table.php │ ├── 2023_01_29_220021_create_job_status_users_table.php │ ├── 2023_02_20_220021_add_selector_to_job_statuses_table.php │ ├── 2023_02_21_175500_add_indexes.php │ └── 2023_03_03_175500_make_job_id_nullable.php ├── public └── dashboard │ ├── assets │ ├── BatchListPage.642ca138.js │ ├── BatchShowPage.fd3feb87.js │ ├── DashboardPage.cb6f9a81.js │ ├── ErrorNotFound.8eff900f.js │ ├── HistoryPage.47e7e93c.js │ ├── JobListPage.904489e7.js │ ├── JobShowPage.66657ec5.js │ ├── KFOkCnqEu92Fr1MmgVxIIzQ.34e9582c.woff │ ├── KFOlCnqEu92Fr1MmEU9fBBc-.9ce7f3ac.woff │ ├── KFOlCnqEu92Fr1MmSU5fBBc-.bf14c7d7.woff │ ├── KFOlCnqEu92Fr1MmWUlfBBc-.e0fd57c0.woff │ ├── KFOlCnqEu92Fr1MmYUtfBBc-.f6537e32.woff │ ├── KFOmCnqEu92Fr1Mu4mxM.f2abf7fb.woff │ ├── MainLayout.9436e4d8.js │ ├── QAvatar.c20229f2.js │ ├── QBanner.2e24d9cb.js │ ├── QBtn.e5691bad.js │ ├── QCard.637ea244.js │ ├── QCircularProgress.04fad857.js │ ├── QItem.8e917815.js │ ├── QPage.edb823d8.js │ ├── QPagination.6eb9fd1c.js │ ├── QueueListPage.f55cfae4.js │ ├── QueueShowPage.e3e55b3a.js │ ├── RunListWithFiltering.35641e08.js │ ├── RunShowPage.48ff0e2b.js │ ├── StatusCount.c6c9d9e0.js │ ├── StatusCount.dafe58ca.css │ ├── api.9fab9f7e.js │ ├── axios.c0010d0a.js │ ├── axios.d530bc82.js │ ├── dayjs.min.96389700.js │ ├── flUhRq6tzZclQEJ-Vdg-IuiaDsNa.fd84f88b.woff │ ├── flUhRq6tzZclQEJ-Vdg-IuiaDsNcIhQ8tQ.83be7b2f.woff2 │ ├── format.a33550d6.js │ ├── i18n.48345f25.js │ ├── index.02176f2b.js │ ├── index.416c10b7.js │ ├── index.94ec6327.css │ ├── localizedFormat.5545e0be.js │ ├── position-engine.2625858a.js │ ├── relativeTime.c4fed282.js │ ├── render.bb221d47.js │ ├── selection.02425b78.js │ └── use-prevent-scroll.cc490b6c.js │ ├── favicon.ico │ ├── icons │ ├── favicon-128x128.png │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ └── favicon-96x96.png │ └── index.html ├── routes ├── api.php └── web.php └── src ├── Concerns ├── InteractsWithSignals.php └── Trackable.php ├── Console ├── ClearJobStatusCommand.php └── ShowJobStatusSummaryCommand.php ├── Dashboard ├── Commands │ └── InstallAssets.php ├── Http │ ├── Composers │ │ └── DashboardVariables.php │ ├── Controllers │ │ └── DashboardController.php │ └── Middleware │ │ └── Authenticate.php └── Utils │ ├── Assets.php │ └── InstalledVersion.php ├── DatabaseConnectorDecorator.php ├── DatabaseQueueDecorator.php ├── Enums ├── MessageType.php └── Status.php ├── Exceptions ├── CannotBeRetriedException.php └── JobCancelledException.php ├── Http ├── Controllers │ └── Api │ │ ├── BatchController.php │ │ ├── Controller.php │ │ ├── JobController.php │ │ ├── JobRetryController.php │ │ ├── JobSignalController.php │ │ ├── QueueController.php │ │ └── RunController.php └── Requests │ ├── Api │ └── Run │ │ └── RunSearchRequest.php │ ├── JobSignalStoreRequest.php │ └── JobStatusSearchRequest.php ├── JobStatusModifier.php ├── JobStatusRepository.php ├── JobStatusServiceProvider.php ├── Listeners ├── BatchDispatched.php ├── JobExceptionOccurred.php ├── JobFailed.php ├── JobProcessed.php ├── JobProcessing.php ├── JobQueued.php ├── JobReleasedAfterException.php └── Utils │ └── Helper.php ├── Models ├── JobBatch.php ├── JobException.php ├── JobMessage.php ├── JobSignal.php ├── JobStatus.php ├── JobStatusStatus.php ├── JobStatusTag.php └── JobStatusUser.php ├── Retry ├── JobRetrier.php ├── JobRetrierFactory.php └── Retrier.php ├── Search ├── Collections │ ├── BatchCollection.php │ ├── JobRunCollection.php │ ├── JobStatusCollection.php │ ├── QueueCollection.php │ └── TrackedJobCollection.php ├── Queries │ ├── PaginateBatches.php │ ├── PaginateJobs.php │ ├── PaginateQueues.php │ └── PaginateRuns.php ├── Result │ ├── Batch.php │ ├── JobRun.php │ ├── Queue.php │ └── TrackedJob.php └── Transformers │ ├── BatchesTransformer.php │ ├── JobsTransformer.php │ ├── QueuesTransformer.php │ └── RunsTransformer.php ├── Share ├── RetrieveConfig.php └── ShareConfig.php └── helpers.php /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | in([ 5 | __DIR__ . '/src', 6 | __DIR__ . '/tests', 7 | __DIR__ . '/database', 8 | __DIR__ . '/config', 9 | __DIR__ . '/routes' 10 | ]); 11 | 12 | return (new PhpCsFixer\Config()) 13 | ->setRules([ 14 | // PSR 1 and 2 standards 15 | '@PSR2' => true, 16 | 17 | // Each line of multi-line DocComments must have an asterisk [PSR-5] and must be aligned with the first one. 18 | 'align_multiline_comment' => ['comment_type' => 'all_multiline'], 19 | 20 | // Each element of an array must be indented exactly once. 21 | 'array_indentation' => true, 22 | 23 | // Arrays must be defined using the short syntax [] rather than the long syntax array() 24 | 'array_syntax' => ['syntax' => 'short'], 25 | 26 | // Ensure there is no code on the same line as the PHP open tag and it is followed by a blank line. 27 | 'blank_line_after_opening_tag' => true, 28 | 29 | // An empty line feed must precede any configured statement (foreach, continue, yield etc). 30 | 'blank_line_before_statement' => true, 31 | 32 | // A single space should be between cast and variable. 33 | 'cast_spaces' => ['space' => 'single'], 34 | 35 | // The PHP constants true, false, and null must be written using lowercase. 36 | 'constant_case' => ['case' => 'lower'], 37 | 38 | // There should be a single space between each concatenation 39 | 'concat_space' => ['spacing' => 'one'], 40 | 41 | // Transforms imported FQCN parameters and return types in function arguments to short version. 42 | 'fully_qualified_strict_types' => true, 43 | 44 | // Class static references self, static and parent MUST be in lower case. 45 | 'lowercase_static_reference' => true, 46 | 47 | // 48 | 'magic_method_casing' => true, 49 | 50 | // Magic method definitions and calls must be using the correct casing. 51 | 'magic_constant_casing' => true, 52 | 53 | // Leave only one space after a method argument comma, and if the signature is multiline ensure all parameters are multiline 54 | 'method_argument_space' => ['keep_multiple_spaces_after_comma' => false, 'on_multiline' => 'ensure_fully_multiline'], 55 | 56 | // Method chaining MUST be properly indented 57 | 'method_chaining_indentation' => true, 58 | 59 | // Ensure multiline comments are either /** or /* 60 | 'multiline_comment_opening_closing' => true, 61 | 62 | // Native functions should be lower case 63 | 'native_function_casing' => true, 64 | 65 | // All instances created with new keyword must be followed by braces. 66 | 'new_with_braces' => true, 67 | 68 | // Native type hints for functions should use the correct case. 69 | 'native_function_type_declaration_casing' => true, 70 | 71 | // here should not be blank lines between docblock and the documented element. 72 | 'no_blank_lines_after_phpdoc' => true, 73 | 74 | // The closing tag MUST be omitted from files containing only PHP. 75 | 'no_closing_tag' => true, 76 | 77 | // There should not be any empty comments. 78 | 'no_empty_comment' => true, 79 | 80 | // There should not be empty PHPDoc blocks. 81 | 'no_empty_phpdoc' => true, 82 | 83 | // Remove useless (semicolon) statements. 84 | 'no_empty_statement' => true, 85 | 86 | // Unused use statements must be removed. 87 | 'no_unused_imports' => true, 88 | 89 | // There should not be useless else cases. 90 | 'no_useless_else' => true, 91 | 92 | // There should not be an empty return statement at the end of a function. 93 | 'no_useless_return' => true, 94 | 95 | // Imports must be ordered alphabetically 96 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 97 | 98 | // Enforce snake case for PHPUnit test methods, following configuration. 99 | 'php_unit_method_casing' => ['case' => 'snake_case'], 100 | 101 | // PHPDoc should contain @param for all params. 102 | 'phpdoc_add_missing_param_annotation' => true, 103 | 104 | // Docblocks should have the same indentation as the documented subject. 105 | 'phpdoc_indent' => true, 106 | 107 | // @return void and @return null annotations should be omitted from PHPDoc. 108 | 'phpdoc_no_empty_return' => true, 109 | 110 | // @package and @subpackage annotations should be omitted from PHPDoc. 111 | 'phpdoc_no_package' => true, 112 | 113 | // Order phpdoc tags by value. 114 | 'phpdoc_order' => true, 115 | 116 | // PHPDoc summary should end in either a full stop, exclamation mark, or question mark. 117 | 'phpdoc_summary' => true, 118 | 119 | // There should be no space before colon, and one space after it in return type declarations 120 | 'return_type_declaration' => ['space_before' => 'none'], 121 | 122 | // There MUST be one use keyword per declaration. 123 | 'single_import_per_statement' => true, 124 | 125 | // Convert single line comments using a hash to use // 126 | 'single_line_comment_style' => ['comment_types' => ['hash']], 127 | 128 | // Convert double quotes to single quotes for simple strings. 129 | 'single_quote' => true, 130 | 131 | // Use null coalescing operator ?? where possible. Requires PHP >= 7.0. 132 | 'ternary_to_null_coalescing' => true, 133 | 134 | // Multi-line arrays must have a trailing comma. 135 | 'trailing_comma_in_multiline' => ['elements' => ['arrays']] 136 | ]) 137 | ->setFinder($finder); 138 | -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Toby Twigger 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | Logo 4 | 5 | 6 |

Laravel Job Tracker

7 | 8 |

9 | The only Laravel job tracking and debugging tool you'll ever need 10 |
11 | Demo Site » 12 |
13 | Explore the docs » 14 |
15 |
16 | Report Bug 17 | · 18 | Request Feature 19 |

20 | 21 | [![Latest Version](https://img.shields.io/github/v/release/tobytwigger/laravel-job-status?label=Latest%20Version&sort=semver&style=plastic)](https://github.com/tobytwigger/laravel-job-status/releases) 22 | [![Build Status](https://github.com/tobytwigger/laravel-job-status/actions/workflows/trigger_main_branch_flow.yml/badge.svg)](https://github.com/tobytwigger/laravel-job-status/actions/workflows/trigger_main_branch_flow.yml) 23 | 24 | [![Buy me a coffee](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](http://buymeacoffee.com/translate) 25 | 26 | ## Contents 27 | 28 | * [About the Project](#about) 29 | * [Documentation](#docs) 30 | * [Contributing](#contributing) 31 | * [Copyright and Licence](#copyright-and-licence) 32 | * [Contact](#contact) 33 | 34 | ## About 35 | 36 | - Save a complete history of jobs running in your Laravel application. 37 | - Provides realtime and historic insights into your queues. 38 | - Add two-way communication, cancelling jobs and logging to jobs. 39 | - Integrates with any JS framework to show realtime feedback to your users. 40 | - Zero configuration required. 41 | 42 | ## Docs 43 | 44 | We've taken care over documenting everything you'll need to get started and use this package fully. 45 | 46 | [Check out the docs](https://tobytwigger.github.io/laravel-job-status) on our documentation site. 47 | 48 | ## Contributing 49 | 50 | Contributions are welcome! Before contributing to this project, familiarize 51 | yourself with [CONTRIBUTING.md](CONTRIBUTING.md). 52 | 53 | Feel free to open an issue or message me on discord (tobytwigger#7153) to chat about this package or potential features. 54 | 55 | You can see the full roadmap in the [discussions](https://github.com/tobytwigger/laravel-job-status/discussions/71). 56 | 57 | ## Copyright and Licence 58 | 59 | This package is copyright © [Toby Twigger](https://github.com/tobytwigger) 60 | and licensed for use under the terms of the MIT License (MIT). Please see 61 | [LICENCE.md](LICENCE.md) for more information. 62 | 63 | ## Contact 64 | 65 | For any questions, suggestions, security vulnerabilities or help, email me directly 66 | at [tobytwigger1@gmail.com](mailto:tobytwigger1@gmail.com) 67 | 68 | ## Screenshots 69 | 70 | ![example of job status in use](https://github.com/tobytwigger/laravel-job-status/blob/main/docs/docs/assets/images/podcast.gif "Showing the user the status of their podcast being uploaded") 71 | 72 | 73 | ![job status dashboard](https://github.com/tobytwigger/laravel-job-status/blob/main/docs/docs/assets/images/dashboard/list-of-jobs.png "Viewing a list of jobs running in your application") 74 | 75 | 76 | ![dashboard timeline](https://github.com/tobytwigger/laravel-job-status/blob/main/docs/docs/assets/images/dashboard/run-timeline.png "View the timeline for a particular run of a job") 77 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "twigger/laravel-job-status", 3 | "description": "Job status for Laravel", 4 | "type": "library", 5 | "keywords": [ 6 | "laravel", 7 | "vue", 8 | "progress", 9 | "track", 10 | "job", 11 | "queue", 12 | "config", 13 | "cancel" 14 | ], 15 | "license": "MIT", 16 | "homepage": "https://github.com/tobytwigger/laravel-job-status", 17 | "readme": "https://github.com/tobytwigger/laravel-job-status/blob/master/README.md", 18 | "authors": [ 19 | { 20 | "name": "Toby Twigger", 21 | "email": "tobytwigger1@gmail.com", 22 | "homepage": "https://github.com/tobytwigger" 23 | } 24 | ], 25 | "support": { 26 | "email": "tobytwigger1@gmail.com", 27 | "issues":"https://github.com/tobytwigger/laravel-job-status/issues", 28 | "docs":"https://tobytwigger.github.io/laravel-job-status/" 29 | }, 30 | "funding": [ 31 | { 32 | "type": "buymeacoffee", 33 | "url": "https://www.buymeacoffee.com/settings" 34 | } 35 | ], 36 | "require": { 37 | "laravel/framework": "^8.0|^9.0|^10.0", 38 | "php": "^8.1", 39 | "doctrine/dbal": "^3.5" 40 | }, 41 | "require-dev": { 42 | "phpunit/phpunit": "^9.4", 43 | "phpspec/prophecy-phpunit": "^2.0", 44 | "orchestra/testbench": "^6.2|^7.0", 45 | "brianium/paratest": "^6.3", 46 | "phpstan/phpstan": "^1.8", 47 | "friendsofphp/php-cs-fixer": "^3.10", 48 | "laravel/serializable-closure": "^1.2" 49 | }, 50 | "suggest": { 51 | }, 52 | "autoload": { 53 | "psr-4": { 54 | "JobStatus\\": "src/" 55 | }, 56 | "files": [ 57 | "src/helpers.php" 58 | ] 59 | }, 60 | "autoload-dev": { 61 | "psr-4": { 62 | "JobStatus\\Tests\\": "tests/", 63 | "JobStatus\\Database\\Factories\\": "database/factories/" 64 | } 65 | }, 66 | "extra": { 67 | "laravel": { 68 | "providers": [ 69 | "JobStatus\\JobStatusServiceProvider" 70 | ] 71 | } 72 | }, 73 | "scripts": { 74 | "test": "vendor/bin/paratest --colors --verbose --configuration phpunit.xml" 75 | }, 76 | "config": { 77 | "allow-plugins": { 78 | "dealerdirect/phpcodesniffer-composer-installer": true 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /config/laravel-job-status.php: -------------------------------------------------------------------------------- 1 | true, 14 | 15 | /* 16 | | 17 | | 18 | | By default, we will only track jobs that run that use the `Trackable` trait. 19 | | 20 | | If this is set to true, we will track information about any job with no configuration needed. 21 | | 22 | */ 23 | 'track_anonymous' => false, 24 | 25 | /* 26 | |-------------------------------------------------------------------------- 27 | | Table Prefix 28 | |-------------------------------------------------------------------------- 29 | | 30 | | Set to prefix all tables with something. Leave empty to name the tables without a prefix 31 | | 32 | | Warning: If this value changes, you will need to change the name of the database tables since the table name 33 | | will not be automatically updated. 34 | */ 35 | 36 | 'table_prefix' => 'job_status', 37 | 38 | /* 39 | |-------------------------------------------------------------------------- 40 | | Route information 41 | |-------------------------------------------------------------------------- 42 | | 43 | | Configuration around the routes this package publishes 44 | | 45 | */ 46 | 47 | 'routes' => [ 48 | 'api' => [ 49 | // The full URL to the API, e.g. https://example.com/api. You may also omit the path after the domain and use the prefix instead. 50 | 'base_url' => null, 51 | // Can be used to append `/api` to the domain. If the `base_url` is https://example.com and the prefix is `api`, the url will be https://example.com/api 52 | 'prefix' => 'api', 53 | // Whether the API is enabled 54 | 'enabled' => true, 55 | // What middleware to apply to the API 56 | // To stop anonymous users accessing public job information, add Auth:API to the middleware array 57 | 'middleware' => ['api'], 58 | ], 59 | ], 60 | 61 | /* 62 | |-------------------------------------------------------------------------- 63 | | Dashboard information 64 | |-------------------------------------------------------------------------- 65 | | 66 | | Configuration around the dashboard to view job history 67 | | 68 | */ 69 | 70 | 'dashboard' => [ 71 | 'enabled' => true, 72 | 73 | // The domain the dashboard is on, or null to host on the main app domain 74 | 'domain' => null, 75 | 76 | // The path where the dashboard will be accessible from. 77 | 'path' => 'job-status', 78 | 79 | // These middleware will get attached onto each route of laravel job status 80 | 'middleware' => ['web'], 81 | 82 | ], 83 | 84 | 85 | /* 86 | |-------------------------------------------------------------------------- 87 | | Job Collectors 88 | |-------------------------------------------------------------------------- 89 | | 90 | | Configure what information we store from a job 91 | | 92 | */ 93 | 94 | 'collectors' => [ 95 | 'messages' => [ 96 | // Whether your job can send messages to your app 97 | 'enabled' => true, 98 | ], 99 | 'signals' => [ 100 | // Whether your app can send signals to your job 101 | 'enabled' => true, 102 | ], 103 | 'status_history' => [ 104 | // Whether we store the timeline of the job 105 | 'enabled' => true, 106 | ], 107 | ], 108 | 109 | ]; 110 | -------------------------------------------------------------------------------- /database/factories/JobBatchFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->bothify('##??##??##??##??#'), 21 | 'name' => $this->faker->word, 22 | ]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /database/factories/JobExceptionFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->sentence, 21 | 'stack_trace' => [$this->faker->word, $this->faker->word], 22 | 'previous_id' => null, 23 | 'line' => $this->faker->numberBetween(1, 100), 24 | 'file' => $this->faker->filePath(), 25 | 'code' => $this->faker->sentence, 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /database/factories/JobMessageFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->sentence, 23 | 'type' => $this->faker->randomElement(MessageType::cases()), 24 | 'job_status_id' => JobStatus::factory(), 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /database/factories/JobSignalFactory.php: -------------------------------------------------------------------------------- 1 | JobStatus::factory(), 23 | 'signal' => $this->faker->word, 24 | 'handled_at' => null, 25 | 'parameters' => [], 26 | 'cancel_job' => $this->faker->boolean, 27 | ]; 28 | } 29 | 30 | public function handled() 31 | { 32 | $this->state(fn (array $attributes) => [ 33 | 'handled_at' => Carbon::now()->subDay(), 34 | ]); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /database/factories/JobStatusFactory.php: -------------------------------------------------------------------------------- 1 | JobFake::class, 25 | 'alias' => $this->faker->unique()->word, 26 | 'queue' => $this->faker->unique()->word, 27 | 'payload' => [], 28 | 'percentage' => $this->faker->numberBetween(0, 100), 29 | 'uuid' => Str::uuid(), 30 | 'job_id' => $this->faker->numberBetween(1, 10000000), 31 | 'connection_name' => 'database', 32 | 'status' => Status::QUEUED, 33 | 'is_unprotected' => true, 34 | ]; 35 | } 36 | 37 | public function withException(string $message) 38 | { 39 | return $this->state(function (array $attributes) use ($message) { 40 | return [ 41 | 'exception_id' => JobException::factory()->create(['message' => $message])->id, 42 | ]; 43 | }); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /database/factories/JobStatusStatusFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->randomElement(Status::cases()), 23 | 'job_status_id' => JobStatus::factory(), 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /database/factories/JobStatusTagFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->word, 22 | 'value' => $this->faker->sentence, 23 | 'is_indexless' => false, 24 | 'job_status_id' => JobStatus::factory(), 25 | ]; 26 | } 27 | 28 | public function indexless(string $key) 29 | { 30 | return $this->state(fn (array $attributes) => [ 31 | 'is_indexless' => true, 32 | 'key' => $key, 33 | 'value' => null, 34 | ]); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /database/factories/JobStatusUserFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->numberBetween(1, 100), 22 | 'job_status_id' => JobStatus::factory(), 23 | ]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /database/migrations/2022_07_24_001000_create_job_statuses_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->uuid('uuid')->nullable(); 17 | $table->text('class'); 18 | $table->string('alias'); 19 | $table->string('queue')->nullable(); 20 | $table->longText('payload')->nullable(); 21 | $table->float('percentage')->default(0.0); 22 | $table->string('status')->default('queued'); 23 | $table->timestamp('started_at', 3)->nullable(); 24 | $table->timestamp('finished_at', 3)->nullable(); 25 | $table->string('job_id'); 26 | $table->boolean('is_unprotected')->default(true); 27 | $table->string('connection_name'); 28 | $table->timestamps(3); 29 | }); 30 | } 31 | 32 | /** 33 | * Reverse the migrations. 34 | * 35 | */ 36 | public function down() 37 | { 38 | Schema::dropIfExists(sprintf('%s_%s', config('laravel-job-status.table_prefix'), 'job_statuses')); 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /database/migrations/2022_07_24_001100_create_job_messages_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->text('message'); 17 | $table->string('type')->default('info'); 18 | $table->unsignedBigInteger('job_status_id'); 19 | $table->timestamps(3); 20 | }); 21 | 22 | Schema::table(sprintf('%s_%s', config('laravel-job-status.table_prefix'), 'job_messages'), function (Blueprint $table) { 23 | $table->foreign('job_status_id')->references('id')->on(sprintf('%s_%s', config('laravel-job-status.table_prefix'), 'job_statuses'))->cascadeOnDelete(); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | * 30 | */ 31 | public function down() 32 | { 33 | Schema::dropIfExists(sprintf('%s_%s', config('laravel-job-status.table_prefix'), 'job_messages')); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /database/migrations/2022_07_24_001200_create_job_signals_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->unsignedBigInteger('job_status_id'); 17 | $table->string('signal'); 18 | $table->dateTime('handled_at', 3)->nullable(); 19 | $table->text('parameters')->nullable(); 20 | $table->boolean('cancel_job')->default(false); 21 | $table->timestamps(3); 22 | }); 23 | 24 | Schema::table(sprintf('%s_%s', config('laravel-job-status.table_prefix'), 'job_signals'), function (Blueprint $table) { 25 | $table->foreign('job_status_id')->references('id')->on(sprintf('%s_%s', config('laravel-job-status.table_prefix'), 'job_statuses'))->cascadeOnDelete(); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | * 32 | */ 33 | public function down() 34 | { 35 | Schema::dropIfExists(sprintf('%s_%s', config('laravel-job-status.table_prefix'), 'job_signals')); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /database/migrations/2022_07_24_001300_create_job_status_tags_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('key'); 17 | $table->string('value')->nullable(); 18 | $table->boolean('is_indexless')->default(false); 19 | $table->unsignedBigInteger('job_status_id'); 20 | $table->timestamps(); 21 | }); 22 | 23 | Schema::table(sprintf('%s_%s', config('laravel-job-status.table_prefix'), 'job_status_tags'), function (Blueprint $table) { 24 | $table->foreign('job_status_id')->references('id')->on(sprintf('%s_%s', config('laravel-job-status.table_prefix'), 'job_statuses'))->cascadeOnDelete(); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | */ 32 | public function down() 33 | { 34 | Schema::dropIfExists(sprintf('%s_%s', config('laravel-job-status.table_prefix'), 'job_status_tags')); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /database/migrations/2022_07_24_001400_create_job_status_statuses_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('status')->default('queued'); 17 | $table->unsignedBigInteger('job_status_id'); 18 | $table->timestamps(3); 19 | }); 20 | 21 | Schema::table(sprintf('%s_%s', config('laravel-job-status.table_prefix'), 'job_status_statuses'), function (Blueprint $table) { 22 | $table->foreign('job_status_id')->references('id')->on(sprintf('%s_%s', config('laravel-job-status.table_prefix'), 'job_statuses'))->cascadeOnDelete(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | */ 30 | public function down() 31 | { 32 | Schema::dropIfExists(sprintf('%s_%s', config('laravel-job-status.table_prefix'), 'job_status_statuses')); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /database/migrations/2023_01_25_205630_create_job_status_exceptions_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 16 | $table->text('message'); 17 | $table->longText('stack_trace'); 18 | $table->unsignedInteger('line')->nullable(); 19 | $table->text('file')->nullable(); 20 | $table->unsignedInteger('code')->nullable(); 21 | $table->unsignedBigInteger('previous_id')->nullable(); 22 | $table->timestamps(3); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | */ 30 | public function down() 31 | { 32 | Schema::dropIfExists(sprintf('%s_%s', config('laravel-job-status.table_prefix'), 'job_exceptions')); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /database/migrations/2023_01_25_235630_link_job_status_exceptions_table_to_job_status_table.php: -------------------------------------------------------------------------------- 1 | unsignedBigInteger('exception_id')->nullable(); 17 | }); 18 | 19 | Schema::table(sprintf('%s_%s', config('laravel-job-status.table_prefix'), 'job_statuses'), function (Blueprint $table) { 20 | $table->foreign('exception_id')->references('id')->on(sprintf('%s_%s', config('laravel-job-status.table_prefix'), 'job_exceptions'))->cascadeOnDelete(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | * 27 | */ 28 | public function down() 29 | { 30 | Schema::table(sprintf('%s_%s', config('laravel-job-status.table_prefix'), 'job_statuses'), function (Blueprint $table) { 31 | if (DB::getDriverName() !== 'sqlite') { 32 | $table->dropForeign(['exception_id']); 33 | } 34 | $table->dropColumn('exception_id'); 35 | }); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /database/migrations/2023_01_26_205630_create_job_status_batches_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 16 | $table->string('batch_id'); 17 | $table->string('name')->nullable(); 18 | $table->timestamps(); 19 | }); 20 | } 21 | 22 | /** 23 | * Reverse the migrations. 24 | * 25 | */ 26 | public function down() 27 | { 28 | Schema::dropIfExists(sprintf('%s_%s', config('laravel-job-status.table_prefix'), 'job_batches')); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /database/migrations/2023_01_26_235630_link_job_status_batches_table_to_job_status_table.php: -------------------------------------------------------------------------------- 1 | unsignedBigInteger('batch_id')->nullable(); 17 | }); 18 | 19 | Schema::table(sprintf('%s_%s', config('laravel-job-status.table_prefix'), 'job_statuses'), function (Blueprint $table) { 20 | $table->foreign('batch_id')->references('id')->on(sprintf('%s_%s', config('laravel-job-status.table_prefix'), 'job_batches'))->cascadeOnDelete(); 21 | }); 22 | } 23 | 24 | /** 25 | * Reverse the migrations. 26 | * 27 | */ 28 | public function down() 29 | { 30 | Schema::table(sprintf('%s_%s', config('laravel-job-status.table_prefix'), 'job_statuses'), function (Blueprint $table) { 31 | if (DB::getDriverName() !== 'sqlite') { 32 | $table->dropForeign(['batch_id']); 33 | } 34 | $table->dropColumn('batch_id'); 35 | }); 36 | } 37 | }; 38 | -------------------------------------------------------------------------------- /database/migrations/2023_01_29_220021_create_job_status_users_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 16 | $table->unsignedBigInteger('user_id'); 17 | $table->unsignedBigInteger('job_status_id'); 18 | $table->timestamps(); 19 | }); 20 | 21 | Schema::table(sprintf('%s_%s', config('laravel-job-status.table_prefix'), 'job_status_users'), function (Blueprint $table) { 22 | $table->foreign('job_status_id')->references('id')->on(sprintf('%s_%s', config('laravel-job-status.table_prefix'), 'job_statuses'))->cascadeOnDelete(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | * 29 | */ 30 | public function down() 31 | { 32 | Schema::dropIfExists(sprintf('%s_%s', config('laravel-job-status.table_prefix'), 'job_status_users')); 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /database/migrations/2023_02_20_220021_add_selector_to_job_statuses_table.php: -------------------------------------------------------------------------------- 1 | string('selector')->nullable(); 16 | }); 17 | $this->updateJobSelectors(); 18 | Schema::table(sprintf('%s_%s', config('laravel-job-status.table_prefix'), 'job_statuses'), function (Blueprint $table) { 19 | $table->string('selector')->nullable(false)->change(); 20 | }); 21 | } 22 | 23 | private function updateJobSelectors() 24 | { 25 | $jobStatuses = \JobStatus\Models\JobStatus::query() 26 | ->withoutEagerLoads() 27 | ->get(); 28 | foreach ($jobStatuses as $jobStatus) { 29 | $jobStatus->selector = $jobStatus->uuid === null 30 | ? $jobStatus->job_id . '-' . $jobStatus->connection_name 31 | : $jobStatus->uuid; 32 | $jobStatus->save(); 33 | } 34 | } 35 | 36 | /** 37 | * Reverse the migrations. 38 | * 39 | */ 40 | public function down() 41 | { 42 | Schema::table(sprintf('%s_%s', config('laravel-job-status.table_prefix'), 'job_statuses'), function (Blueprint $table) { 43 | $table->dropColumn('selector'); 44 | }); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /database/migrations/2023_02_21_175500_add_indexes.php: -------------------------------------------------------------------------------- 1 | unique(['queue', 'status', 'selector', 'id'], algorithm: 'hash'); 17 | 18 | // Helps to calculate the number of jobs of a certain type in a queue 19 | $table->unique(['batch_id', 'status', 'selector', 'id'], algorithm: 'hash'); 20 | 21 | // Helps to calculate the number of jobs of a certain type in a queue 22 | $table->unique(['alias', 'status', 'selector', 'id'], algorithm: 'hash'); 23 | 24 | 25 | // // Queues are grouped by queue, and therefore this index speeds the rows scanned from x to y. 26 | $table->unique(['queue', 'selector', 'created_at', 'id'], algorithm: 'hash'); 27 | // 28 | // // Jobs are grouped by alias, and therefore this index speeds the rows scanned from x to y. 29 | $table->unique(['alias','selector', 'created_at', 'id'], algorithm: 'hash'); 30 | // 31 | // // Batches are grouped by batch_id, and therefore this index speeds the rows scanned from x to y. 32 | $table->unique(['batch_id','selector', 'created_at', 'id'], algorithm: 'hash'); 33 | // 34 | // // We nearly always order by created_at and id, so this index speeds that up. 35 | $table->unique(['selector', 'created_at', 'id'], algorithm: 'hash'); 36 | }); 37 | } 38 | 39 | /** 40 | * Reverse the migrations. 41 | * 42 | */ 43 | public function down() 44 | { 45 | Schema::table(sprintf('%s_%s', config('laravel-job-status.table_prefix'), 'job_statuses'), function (Blueprint $table) { 46 | $table->dropUnique(['queue', 'status', 'selector', 'id']); 47 | 48 | $table->dropUnique(['batch_id', 'status', 'selector', 'id']); 49 | 50 | $table->dropUnique(['alias', 'status', 'selector', 'id']); 51 | 52 | 53 | // // Queues are grouped by queue, and therefore this index speeds the rows scanned from x to y. 54 | $table->dropUnique(['queue', 'selector', 'created_at', 'id']); 55 | // 56 | // // Jobs are grouped by alias, and therefore this index speeds the rows scanned from x to y. 57 | $table->dropUnique(['alias', 'selector', 'created_at', 'id']); 58 | // 59 | // // Batches are grouped by batch_id, and therefore this index speeds the rows scanned from x to y. 60 | $table->dropUnique(['batch_id', 'selector', 'created_at', 'id']); 61 | // 62 | // // We nearly always order by created_at and id, so this index speeds that up. 63 | $table->dropUnique(['selector', 'created_at', 'id']); 64 | }); 65 | } 66 | }; 67 | -------------------------------------------------------------------------------- /database/migrations/2023_03_03_175500_make_job_id_nullable.php: -------------------------------------------------------------------------------- 1 | string('job_id')->nullable()->change(); 16 | }); 17 | } 18 | 19 | /** 20 | * Reverse the migrations. 21 | * 22 | */ 23 | public function down() 24 | { 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /public/dashboard/assets/BatchListPage.642ca138.js: -------------------------------------------------------------------------------- 1 | import{c as R,Q as S,b as U,d as I}from"./index.416c10b7.js";import{Q as o,b as p,c as g,d as z}from"./QItem.8e917815.js";import{h as T}from"./QPagination.6eb9fd1c.js";import{Q as f,R as q}from"./QBtn.e5691bad.js";import{Q as $}from"./QPage.edb823d8.js";import{S as r}from"./StatusCount.c6c9d9e0.js";import{d as m}from"./dayjs.min.96389700.js";import{l as j}from"./localizedFormat.5545e0be.js";import{r as D}from"./relativeTime.c4fed282.js";import{H as F,c as C,I as l,J as i,K as a,d as e,M as _,N as P,U as V,P as x,r as v,w as E,o as H,k as M,Q as u,F as J,R as K,L as G,A}from"./index.02176f2b.js";import"./render.bb221d47.js";import"./axios.d530bc82.js";import"./format.a33550d6.js";import"./position-engine.2625858a.js";import"./selection.02425b78.js";const O={class:"text-weight-medium"},W={class:"text-grey-8 q-gutter-xs"},X=F({__name:"BatchListItem",props:{batch:null},setup(Q){const t=Q;m.extend(D),m.extend(j);const c=C(()=>m().to(t.batch.created_at)),s=C(()=>t.batch.name!==null&&t.batch.name!==""?t.batch.name:"Batch dispatched at "+m(t.batch.created_at).format("L LTS"));return(h,k)=>(l(),i(g,{clickable:"",to:{path:"/batch/"+t.batch.id}},{default:a(()=>[e(o,{avatar:"",top:""},{default:a(()=>[e(f,{name:"group_work",color:"black",size:"34px"})]),_:1}),e(o,{top:"",class:"col-2 gt-sm"},{default:a(()=>[e(p,{class:"q-mt-sm"},{default:a(()=>[_(P(V(c)),1)]),_:1})]),_:1}),e(o,{top:""},{default:a(()=>[e(p,{lines:"1"},{default:a(()=>[x("span",O,P(V(s)),1)]),_:1}),e(p,{caption:"",lines:"5"},{default:a(()=>[e(r,{count:t.batch.queued,label:"Queued",color:"primary"},null,8,["count"]),e(r,{count:t.batch.started,label:"Running",color:"info"},null,8,["count"]),e(r,{count:t.batch.cancelled,label:"Cancelled",color:"warning"},null,8,["count"]),e(r,{count:t.batch.failed,label:"Failed",color:"negative"},null,8,["count"]),e(r,{count:t.batch.succeeded,label:"Succeeded",color:"positive"},null,8,["count"])]),_:1})]),_:1}),e(o,{top:"",side:""},{default:a(()=>[x("div",W,[e(f,{class:"gt-xs",size:"12px",icon:"keyboard_arrow_right"})])]),_:1})]),_:1},8,["to"]))}}),Y={key:0},Z={class:"q-pa-lg flex flex-center"},ee={key:1},te={key:2},be=F({__name:"BatchListPage",setup(Q){const t=v(null),c=v(1);E(c,(n,b)=>{h()});const s=v(null);function h(){s.value!==null&&s.value.stop(),s.value=R.batches.search().page(c.value).bypassAuth().listen().onUpdated(n=>t.value=n).start()}H(()=>h()),M(()=>{s.value!==null&&s.value.stop()});function k(n){return n.batch_id}return(n,b)=>(l(),i($,{class:"justify-evenly",padding:""},{default:a(()=>[e(U,null,{default:a(()=>[e(S,{icon:"list",to:"/batch",label:"Batches"}),e(S,{to:"/batch",label:"List Batches"})]),_:1}),e(z,{class:"rounded-borders q-pa-lg"},{default:a(()=>{var y,B,L,w,N;return[e(p,{header:""},{default:a(()=>[_("All Batches")]),_:1}),((y=t.value)==null?void 0:y.total)>0?(l(),u("div",Y,[e(I),(l(!0),u(J,null,K((L=(B=t.value)==null?void 0:B.data)!=null?L:[],d=>(l(),u("div",{key:k(d)},[e(X,{batch:d},null,8,["batch"]),e(I)]))),128)),x("div",Z,[((w=t.value)==null?void 0:w.total)>0?(l(),i(T,{key:0,input:"","model-value":t.value.current_page,"onUpdate:modelValue":b[0]||(b[0]=d=>c.value=d),max:t.value.last_page},null,8,["model-value","max"])):G("",!0)])])):((N=t.value)==null?void 0:N.total)===0?(l(),u("div",ee,[A((l(),i(g,{clickable:""},{default:a(()=>[e(o,{avatar:""},{default:a(()=>[e(f,{color:"negative",name:"warning"})]),_:1}),e(o,null,{default:a(()=>[_("No batches found")]),_:1})]),_:1})),[[q]])])):(l(),u("div",te,[A((l(),i(g,{clickable:""},{default:a(()=>[e(o,{avatar:""},{default:a(()=>[e(f,{color:"primary",name:"sync"})]),_:1}),e(o,null,{default:a(()=>[_("Loading")]),_:1})]),_:1})),[[q]])]))]}),_:1})]),_:1}))}});export{be as default}; 2 | -------------------------------------------------------------------------------- /public/dashboard/assets/BatchShowPage.fd3feb87.js: -------------------------------------------------------------------------------- 1 | import{c as y,Q as i,b as B}from"./index.416c10b7.js";import{c as u,Q as m,b as o,d as I}from"./QItem.8e917815.js";import{Q as p}from"./QPage.edb823d8.js";import{d as Q}from"./dayjs.min.96389700.js";import{_ as g}from"./RunListWithFiltering.35641e08.js";import{H as L,r as N,o as k,k as w,c as x,I as f,J as _,K as a,d as t,P as n,M as l,N as h,U as b}from"./index.02176f2b.js";import"./QBtn.e5691bad.js";import"./render.bb221d47.js";import"./axios.d530bc82.js";import"./QPagination.6eb9fd1c.js";import"./format.a33550d6.js";import"./QCircularProgress.04fad857.js";import"./api.9fab9f7e.js";import"./relativeTime.c4fed282.js";const S={class:"row"},j={class:"col-12 q-py-md"},P={class:"col-12"},z=L({__name:"BatchShowPage",props:{batchId:null},setup(v){const s=v,e=N(null);k(()=>{let r=y.batches.show(s.batchId).bypassAuth().listen().onUpdated(d=>e.value=d).start();w(()=>{r.stop()})});const c=x(()=>e.value===null?"Loading":e.value.name!==null&&e.value.name!==""?e.value.name:"dispatched at "+Q(e.value.created_at).format("L LTS"));return(r,d)=>e.value!==null?(f(),_(p,{key:0,class:"justify-evenly",padding:""},{default:a(()=>[t(B,null,{default:a(()=>[t(i,{icon:"list",to:"/batch",label:"Batches"}),t(i,{icon:"list",to:"/batch/"+s.batchId,label:"Batch #"+s.batchId},null,8,["to","label"])]),_:1}),n("div",S,[n("div",j,[t(I,{bordered:"",separator:""},{default:a(()=>[t(u,null,{default:a(()=>[t(m,null,{default:a(()=>[t(o,null,{default:a(()=>[l(h(e.value.batch_id),1)]),_:1}),t(o,{caption:""},{default:a(()=>[l("Batch ID")]),_:1})]),_:1})]),_:1}),t(u,null,{default:a(()=>[t(m,null,{default:a(()=>[t(o,null,{default:a(()=>[l(h(b(c)),1)]),_:1}),t(o,{caption:""},{default:a(()=>[l("Name")]),_:1})]),_:1})]),_:1})]),_:1})]),n("div",P,[t(g,{title:"Runs in batch '"+b(c)+"'","batch-ids":[s.batchId]},null,8,["title","batch-ids"])])])]),_:1})):(f(),_(p,{key:1,class:"items-center justify-evenly",padding:""},{default:a(()=>[l(" Loading ")]),_:1}))}});export{z as default}; 2 | -------------------------------------------------------------------------------- /public/dashboard/assets/DashboardPage.cb6f9a81.js: -------------------------------------------------------------------------------- 1 | import{Q as e}from"./QPage.edb823d8.js";import{_ as a,H as t,I as o,J as s,K as r,M as n}from"./index.02176f2b.js";import"./render.bb221d47.js";const c=t({name:"DashboardPage"});function p(d,i,f,m,_,l){return o(),s(e,{class:"justify-evenly",padding:""},{default:r(()=>[n(" Not implemented... ")]),_:1})}var h=a(c,[["render",p]]);export{h as default}; 2 | -------------------------------------------------------------------------------- /public/dashboard/assets/ErrorNotFound.8eff900f.js: -------------------------------------------------------------------------------- 1 | import{a as t}from"./QBtn.e5691bad.js";import{_ as o,H as s,I as r,Q as a,P as e,d as n}from"./index.02176f2b.js";import"./render.bb221d47.js";const c=s({name:"ErrorNotFound"}),l={class:"fullscreen bg-blue text-white text-center q-pa-md flex flex-center"},d=e("div",{style:{"font-size":"30vh"}},"404",-1),i=e("div",{class:"text-h2",style:{opacity:"0.4"}},"Oops. Nothing here...",-1);function p(_,f,m,u,h,x){return r(),a("div",l,[e("div",null,[d,i,n(t,{class:"q-mt-xl",color:"white","text-color":"blue",unelevated:"",to:"/",label:"Go Home","no-caps":""})])])}var B=o(c,[["render",p]]);export{B as default}; 2 | -------------------------------------------------------------------------------- /public/dashboard/assets/JobListPage.904489e7.js: -------------------------------------------------------------------------------- 1 | import{c as P,Q as j,b as V,d as w}from"./index.416c10b7.js";import{Q as l,b as v,c as k,d as R}from"./QItem.8e917815.js";import{h as $}from"./QPagination.6eb9fd1c.js";import{Q as m,R as B}from"./QBtn.e5691bad.js";import{Q as A}from"./QPage.edb823d8.js";import{S as u}from"./StatusCount.c6c9d9e0.js";import{H as S,I as o,J as i,K as t,d as e,P as g,N as I,Q as n,L as q,r as b,w as F,o as U,k as z,M as f,F as D,R as E,A as N}from"./index.02176f2b.js";import"./render.bb221d47.js";import"./axios.d530bc82.js";import"./format.a33550d6.js";import"./position-engine.2625858a.js";import"./selection.02425b78.js";const H={class:"text-weight-medium"},M={key:0,class:"text-grey-8"},T={class:"text-grey-8 q-gutter-xs"},K=S({__name:"TrackedJobListItem",props:{trackedJob:null},setup(h){const a=h;return(d,s)=>(o(),i(k,{clickable:"",to:{path:"/jobs/"+a.trackedJob.alias}},{default:t(()=>[e(l,{avatar:"",top:""},{default:t(()=>[e(m,{name:"account_tree",color:"black",size:"34px"})]),_:1}),e(l,{top:""},{default:t(()=>[e(v,{lines:"1"},{default:t(()=>{var r;return[g("span",H,I((r=a.trackedJob.alias)!=null?r:a.trackedJob.class),1),a.trackedJob.alias!==a.trackedJob.class?(o(),n("span",M," - "+I(a.trackedJob.class),1)):q("",!0)]}),_:1}),e(v,{caption:"",lines:"5"},{default:t(()=>[e(u,{count:a.trackedJob.queued,label:"Queued",color:"primary"},null,8,["count"]),e(u,{count:a.trackedJob.queued,label:"Running",color:"info"},null,8,["count"]),e(u,{count:a.trackedJob.cancelled,label:"Cancelled",color:"warning"},null,8,["count"]),e(u,{count:a.trackedJob.failed,label:"Failed",color:"negative"},null,8,["count"]),e(u,{count:a.trackedJob.successful,label:"Succeeded",color:"positive"},null,8,["count"])]),_:1})]),_:1}),e(l,{top:"",side:""},{default:t(()=>[g("div",T,[e(m,{class:"gt-xs",size:"12px",icon:"keyboard_arrow_right"})])]),_:1})]),_:1},8,["to"]))}}),G={key:0},O={class:"q-pa-lg flex flex-center"},W={key:1},X={key:2},ie=S({__name:"JobListPage",setup(h){const a=b(null),d=b(1);F(d,(c,_)=>{r()});const s=b(null);function r(){s.value!==null&&s.value.stop(),s.value=P.jobs.search().page(d.value).bypassAuth().listen().onUpdated(c=>a.value=c).start()}U(()=>r()),z(()=>{s.value!==null&&s.value.stop()});function C(c){return c.class}return(c,_)=>(o(),i(A,{class:"justify-evenly",padding:""},{default:t(()=>[e(V,null,{default:t(()=>[e(j,{icon:"list",to:"/jobs",label:"Jobs"}),e(j,{to:"/jobs",label:"List Jobs"})]),_:1}),e(R,{class:"rounded-borders q-pa-lg"},{default:t(()=>{var J,x,y,Q,L;return[e(v,{header:""},{default:t(()=>[f("All Jobs")]),_:1}),((J=a.value)==null?void 0:J.total)>0?(o(),n("div",G,[e(w),(o(!0),n(D,null,E((y=(x=a.value)==null?void 0:x.data)!=null?y:[],p=>(o(),n("div",{key:C(p)},[e(K,{"tracked-job":p},null,8,["tracked-job"]),e(w)]))),128)),g("div",O,[((Q=a.value)==null?void 0:Q.total)>0?(o(),i($,{key:0,input:"","model-value":a.value.current_page,"onUpdate:modelValue":_[0]||(_[0]=p=>d.value=p),max:a.value.last_page},null,8,["model-value","max"])):q("",!0)])])):((L=a.value)==null?void 0:L.total)===0?(o(),n("div",W,[N((o(),i(k,{clickable:""},{default:t(()=>[e(l,{avatar:""},{default:t(()=>[e(m,{color:"negative",name:"warning"})]),_:1}),e(l,null,{default:t(()=>[f("No jobs found")]),_:1})]),_:1})),[[B]])])):(o(),n("div",X,[N((o(),i(k,{clickable:""},{default:t(()=>[e(l,{avatar:""},{default:t(()=>[e(m,{color:"primary",name:"sync"})]),_:1}),e(l,null,{default:t(()=>[f("Loading")]),_:1})]),_:1})),[[B]])]))]}),_:1})]),_:1}))}});export{ie as default}; 2 | -------------------------------------------------------------------------------- /public/dashboard/assets/JobShowPage.66657ec5.js: -------------------------------------------------------------------------------- 1 | import{c as F,Q as v,b as k}from"./index.416c10b7.js";import{Q as d,b as n,c as _,d as w}from"./QItem.8e917815.js";import{Q as h}from"./QPage.edb823d8.js";import{R as j}from"./QBtn.e5691bad.js";import{Q as x}from"./QAvatar.c20229f2.js";import{H as p,I as l,J as r,K as e,d as a,M as o,N as f,P as c,Q as y,F as R,R as B,r as Q,o as L,k as I,A as g}from"./index.02176f2b.js";import{_ as J}from"./RunListWithFiltering.35641e08.js";import"./render.bb221d47.js";import"./axios.d530bc82.js";import"./QPagination.6eb9fd1c.js";import"./format.a33550d6.js";import"./QCircularProgress.04fad857.js";import"./api.9fab9f7e.js";import"./dayjs.min.96389700.js";import"./relativeTime.c4fed282.js";const $={class:"text-weight-medium"},A=p({__name:"JobFailureListItem",props:{jobFailure:null},setup(u){const i=u;return(s,t)=>(l(),r(_,null,{default:e(()=>[a(d,{avatar:"",top:""},{default:e(()=>[a(x,{color:"primary","text-color":"white"},{default:e(()=>[o(f(i.jobFailure.count),1)]),_:1})]),_:1}),a(d,{top:""},{default:e(()=>[a(n,{lines:"1"},{default:e(()=>[c("span",$,f(i.jobFailure.message),1)]),_:1})]),_:1})]),_:1}))}}),N={key:1},C=p({__name:"JobFailureReasons",props:{jobFailureReasons:null},setup(u){return(i,s)=>(l(),r(w,{bordered:"",separator:""},{default:e(()=>[a(n,{header:""},{default:e(()=>[o("Errors")]),_:1}),u.jobFailureReasons.length===0?(l(),r(_,{key:0},{default:e(()=>[a(d,null,{default:e(()=>[a(n,null,{default:e(()=>[o("Good news; no errors found!")]),_:1})]),_:1})]),_:1})):(l(),y("div",N,[(l(!0),y(R,null,B(u.jobFailureReasons,(t,m)=>(l(),r(_,{key:m},{default:e(()=>[a(A,{"job-failure":t},null,8,["job-failure"])]),_:2},1024))),128))]))]),_:1}))}}),E={class:"row"},P={class:"col-12 col-sm-6 q-py-md"},S={class:"col-12 col-sm-6 q-py-md"},V={class:"row"},q={class:"col-12"},se=p({__name:"JobShowPage",props:{alias:null},setup(u){const i=u,s=Q(null),t=Q(null);function m(){t.value!==null&&t.value.stop(),t.value=F.jobs.show(i.alias).bypassAuth().listen().onUpdated(b=>s.value=b).start()}return L(()=>m()),I(()=>{t.value!==null&&t.value.stop()}),(b,D)=>s.value!==null?(l(),r(h,{key:0,class:"justify-evenly",padding:""},{default:e(()=>[a(k,null,{default:e(()=>[a(v,{icon:"list",to:"/jobs",label:"Jobs"}),a(v,{label:s.value.alias,icon:"view_stream",to:"/jobs/"+s.value.alias},null,8,["label","to"])]),_:1}),c("div",E,[c("div",P,[a(w,{bordered:"",separator:""},{default:e(()=>[g((l(),r(_,null,{default:e(()=>[a(d,null,{default:e(()=>[a(n,null,{default:e(()=>[o(f(s.value.alias),1)]),_:1}),a(n,{caption:""},{default:e(()=>[o("Alias")]),_:1})]),_:1})]),_:1})),[[j]]),g((l(),r(_,null,{default:e(()=>[a(d,null,{default:e(()=>[a(n,null,{default:e(()=>[o(f(s.value.class),1)]),_:1}),a(n,{caption:""},{default:e(()=>[o("Class")]),_:1})]),_:1})]),_:1})),[[j]])]),_:1})]),c("div",S,[a(C,{"job-failure-reasons":s.value.failure_reasons},null,8,["job-failure-reasons"])])]),c("div",V,[c("div",q,[a(J,{title:"Runs",aliases:[i.alias]},null,8,["aliases"])])])]),_:1})):(l(),r(h,{key:1,class:"items-center justify-evenly",padding:""},{default:e(()=>[o(" Loading ")]),_:1}))}});export{se as default}; 2 | -------------------------------------------------------------------------------- /public/dashboard/assets/KFOkCnqEu92Fr1MmgVxIIzQ.34e9582c.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobytwigger/laravel-job-status/8f6d4a0d609a6a46c3134a0cb171954afd4ee988/public/dashboard/assets/KFOkCnqEu92Fr1MmgVxIIzQ.34e9582c.woff -------------------------------------------------------------------------------- /public/dashboard/assets/KFOlCnqEu92Fr1MmEU9fBBc-.9ce7f3ac.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobytwigger/laravel-job-status/8f6d4a0d609a6a46c3134a0cb171954afd4ee988/public/dashboard/assets/KFOlCnqEu92Fr1MmEU9fBBc-.9ce7f3ac.woff -------------------------------------------------------------------------------- /public/dashboard/assets/KFOlCnqEu92Fr1MmSU5fBBc-.bf14c7d7.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobytwigger/laravel-job-status/8f6d4a0d609a6a46c3134a0cb171954afd4ee988/public/dashboard/assets/KFOlCnqEu92Fr1MmSU5fBBc-.bf14c7d7.woff -------------------------------------------------------------------------------- /public/dashboard/assets/KFOlCnqEu92Fr1MmWUlfBBc-.e0fd57c0.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobytwigger/laravel-job-status/8f6d4a0d609a6a46c3134a0cb171954afd4ee988/public/dashboard/assets/KFOlCnqEu92Fr1MmWUlfBBc-.e0fd57c0.woff -------------------------------------------------------------------------------- /public/dashboard/assets/KFOlCnqEu92Fr1MmYUtfBBc-.f6537e32.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobytwigger/laravel-job-status/8f6d4a0d609a6a46c3134a0cb171954afd4ee988/public/dashboard/assets/KFOlCnqEu92Fr1MmYUtfBBc-.f6537e32.woff -------------------------------------------------------------------------------- /public/dashboard/assets/KFOmCnqEu92Fr1Mu4mxM.f2abf7fb.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobytwigger/laravel-job-status/8f6d4a0d609a6a46c3134a0cb171954afd4ee988/public/dashboard/assets/KFOmCnqEu92Fr1Mu4mxM.f2abf7fb.woff -------------------------------------------------------------------------------- /public/dashboard/assets/QAvatar.c20229f2.js: -------------------------------------------------------------------------------- 1 | import{k as i,l as s,Q as u}from"./QBtn.e5691bad.js";import{c as d,f as v}from"./render.bb221d47.js";import{c as o,h as t}from"./index.02176f2b.js";var q=d({name:"QAvatar",props:{...i,fontSize:String,color:String,textColor:String,icon:String,square:Boolean,rounded:Boolean},setup(e,{slots:a}){const n=s(e),r=o(()=>"q-avatar"+(e.color?` bg-${e.color}`:"")+(e.textColor?` text-${e.textColor} q-chip--colored`:"")+(e.square===!0?" q-avatar--square":e.rounded===!0?" rounded-borders":"")),l=o(()=>e.fontSize?{fontSize:e.fontSize}:null);return()=>{const c=e.icon!==void 0?[t(u,{name:e.icon})]:void 0;return t("div",{class:r.value,style:n.value},[t("div",{class:"q-avatar__content row flex-center overflow-hidden",style:l.value},v(a.default,c))])}}});export{q as Q}; 2 | -------------------------------------------------------------------------------- /public/dashboard/assets/QBanner.2e24d9cb.js: -------------------------------------------------------------------------------- 1 | import{r as O,E as B,o as v,k as m,n as D,z as g,h as d,g as w,D as p,c as y}from"./index.02176f2b.js";import{c as k,h as f}from"./render.bb221d47.js";import{u as R,a as x}from"./QItem.8e917815.js";function C(){const t=O(!B.value);return t.value===!1&&v(()=>{t.value=!0}),t}const q=typeof ResizeObserver!="undefined",z=q===!0?{}:{style:"display:block;position:absolute;top:0;left:0;right:0;bottom:0;height:100%;width:100%;overflow:hidden;pointer-events:none;z-index:-1;",url:"about:blank"};var j=k({name:"QResizeObserver",props:{debounce:{type:[String,Number],default:100}},emits:["resize"],setup(t,{emit:e}){let n=null,r,u={width:-1,height:-1};function l(i){i===!0||t.debounce===0||t.debounce==="0"?s():n===null&&(n=setTimeout(s,t.debounce))}function s(){if(n!==null&&(clearTimeout(n),n=null),r){const{offsetWidth:i,offsetHeight:o}=r;(i!==u.width||o!==u.height)&&(u={width:i,height:o},e("resize",u))}}const{proxy:a}=w();if(q===!0){let i;const o=c=>{r=a.$el.parentNode,r?(i=new ResizeObserver(l),i.observe(r),s()):c!==!0&&g(()=>{o(!0)})};return v(()=>{o()}),m(()=>{n!==null&&clearTimeout(n),i!==void 0&&(i.disconnect!==void 0?i.disconnect():r&&i.unobserve(r))}),D}else{let c=function(){n!==null&&(clearTimeout(n),n=null),o!==void 0&&(o.removeEventListener!==void 0&&o.removeEventListener("resize",l,p.passive),o=void 0)},b=function(){c(),r&&r.contentDocument&&(o=r.contentDocument.defaultView,o.addEventListener("resize",l,p.passive),s())};const i=C();let o;return v(()=>{g(()=>{r=a.$el,r&&b()})}),m(c),a.trigger=l,()=>{if(i.value===!0)return d("object",{style:z.style,tabindex:-1,type:"text/html",data:z.url,"aria-hidden":"true",onLoad:b})}}}});const h={left:!0,right:!0,up:!0,down:!0,horizontal:!0,vertical:!0},E=Object.keys(h);h.all=!0;function Q(t){const e={};for(const n of E)t[n]===!0&&(e[n]=!0);return Object.keys(e).length===0?h:(e.horizontal===!0?e.left=e.right=!0:e.left===!0&&e.right===!0&&(e.horizontal=!0),e.vertical===!0?e.up=e.down=!0:e.up===!0&&e.down===!0&&(e.vertical=!0),e.horizontal===!0&&e.vertical===!0&&(e.all=!0),e)}function A(t,e){return e.event===void 0&&t.target!==void 0&&t.target.draggable!==!0&&typeof e.handler=="function"&&t.target.nodeName.toUpperCase()!=="INPUT"&&(t.qClonedBy===void 0||t.qClonedBy.indexOf(e.uid)===-1)}var N=k({name:"QBanner",props:{...R,inlineActions:Boolean,dense:Boolean,rounded:Boolean},setup(t,{slots:e}){const{proxy:{$q:n}}=w(),r=x(t,n),u=y(()=>"q-banner row items-center"+(t.dense===!0?" q-banner--dense":"")+(r.value===!0?" q-banner--dark q-dark":"")+(t.rounded===!0?" rounded-borders":"")),l=y(()=>`q-banner__actions row items-center justify-end col-${t.inlineActions===!0?"auto":"all"}`);return()=>{const s=[d("div",{class:"q-banner__avatar col-auto row items-center self-start"},f(e.avatar)),d("div",{class:"q-banner__content col text-body2"},f(e.default))],a=f(e.action);return a!==void 0&&s.push(d("div",{class:l.value},a)),d("div",{class:u.value+(t.inlineActions===!1&&a!==void 0?" q-banner--top-padding":""),role:"alert"},s)}}});export{j as Q,N as a,Q as g,A as s}; 2 | -------------------------------------------------------------------------------- /public/dashboard/assets/QCard.637ea244.js: -------------------------------------------------------------------------------- 1 | import{m as j,V as ge,r as Q,c as s,w as R,v as he,h as y,W as me,k as ye,g as z,y as ke}from"./index.02176f2b.js";import{d as be,u as qe,v as H,e as xe,a as Ce,b as we,f as Se,g as Pe,c as Te,r as _,s as Oe,p as W,h as Be,i as Ee}from"./position-engine.2625858a.js";import{u as pe,a as Fe,b as De,d as Ae,g as Ke}from"./selection.02425b78.js";import{u as I,a as U}from"./QItem.8e917815.js";import{c as S,h as P}from"./render.bb221d47.js";import{e as Me}from"./QBtn.e5691bad.js";import{a as Le}from"./index.416c10b7.js";const r=[];let f;function Qe(e){f=e.keyCode===27}function Re(){f===!0&&(f=!1)}function He(e){f===!0&&(f=!1,ge(e,27)===!0&&r[r.length-1](e))}function V(e){window[e]("keydown",Qe),window[e]("blur",Re),window[e]("keyup",He),f=!1}function _e(e){j.is.desktop===!0&&(r.push(e),r.length===1&&V("addEventListener"))}function $(e){const o=r.indexOf(e);o>-1&&(r.splice(o,1),r.length===0&&V("removeEventListener"))}const l=[];function G(e){l[l.length-1](e)}function We(e){j.is.desktop===!0&&(l.push(e),l.length===1&&document.body.addEventListener("focusin",G))}function $e(e){const o=l.indexOf(e);o>-1&&(l.splice(o,1),l.length===0&&document.body.removeEventListener("focusin",G))}var Xe=S({name:"QMenu",inheritAttrs:!1,props:{...be,...pe,...I,...qe,persistent:Boolean,autoClose:Boolean,separateClosePopup:Boolean,noRouteDismiss:Boolean,noRefocus:Boolean,noFocus:Boolean,fit:Boolean,cover:Boolean,square:Boolean,anchor:{type:String,validator:H},self:{type:String,validator:H},offset:{type:Array,validator:xe},scrollTarget:{default:void 0},touchPosition:Boolean,maxHeight:{type:String,default:null},maxWidth:{type:String,default:null}},emits:[...Fe,"click","escapeKey"],setup(e,{slots:o,emit:n,attrs:v}){let a=null,m,g,k;const T=z(),{proxy:b}=T,{$q:u}=b,i=Q(null),c=Q(!1),J=s(()=>e.persistent!==!0&&e.noRouteDismiss!==!0),N=U(e,u),{registerTick:X,removeTick:Y}=Ce(),{registerTimeout:O}=De(),{transitionProps:Z,transitionStyle:ee}=we(e),{localScrollTarget:B,changeScrollEvent:te,unconfigureScrollTarget:oe}=Se(e,M),{anchorEl:d,canShow:ne}=Pe({showing:c}),{hide:E}=Ae({showing:c,canShow:ne,handleShow:re,handleHide:le,hideOnRouteChange:J,processOnMount:!0}),{showPortal:p,hidePortal:F,renderPortal:ae}=Te(T,i,de,"menu"),q={anchorEl:d,innerRef:i,onClickOutside(t){if(e.persistent!==!0&&c.value===!0)return E(t),(t.type==="touchstart"||t.target.classList.contains("q-dialog__backdrop"))&&ke(t),!0}},D=s(()=>W(e.anchor||(e.cover===!0?"center middle":"bottom start"),u.lang.rtl)),se=s(()=>e.cover===!0?D.value:W(e.self||"top start",u.lang.rtl)),ue=s(()=>(e.square===!0?" q-menu--square":"")+(N.value===!0?" q-menu--dark q-dark":"")),ie=s(()=>e.autoClose===!0?{onClick:ce}:{}),A=s(()=>c.value===!0&&e.persistent!==!0);R(A,t=>{t===!0?(_e(C),Be(q)):($(C),_(q))});function x(){Le(()=>{let t=i.value;t&&t.contains(document.activeElement)!==!0&&(t=t.querySelector("[autofocus][tabindex], [data-autofocus][tabindex]")||t.querySelector("[autofocus] [tabindex], [data-autofocus] [tabindex]")||t.querySelector("[autofocus], [data-autofocus]")||t,t.focus({preventScroll:!0}))})}function re(t){if(a=e.noRefocus===!1?document.activeElement:null,We(L),p(),M(),m=void 0,t!==void 0&&(e.touchPosition||e.contextMenu)){const w=he(t);if(w.left!==void 0){const{top:fe,left:ve}=d.value.getBoundingClientRect();m={left:w.left-ve,top:w.top-fe}}}g===void 0&&(g=R(()=>u.screen.width+"|"+u.screen.height+"|"+e.self+"|"+e.anchor+"|"+u.lang.rtl,h)),e.noFocus!==!0&&document.activeElement.blur(),X(()=>{h(),e.noFocus!==!0&&x()}),O(()=>{u.platform.is.ios===!0&&(k=e.autoClose,i.value.click()),h(),p(!0),n("show",t)},e.transitionDuration)}function le(t){Y(),F(),K(!0),a!==null&&(t===void 0||t.qClickOutside!==!0)&&(((t&&t.type.indexOf("key")===0?a.closest('[tabindex]:not([tabindex^="-"])'):void 0)||a).focus(),a=null),O(()=>{F(!0),n("hide",t)},e.transitionDuration)}function K(t){m=void 0,g!==void 0&&(g(),g=void 0),(t===!0||c.value===!0)&&($e(L),oe(),_(q),$(C)),t!==!0&&(a=null)}function M(){(d.value!==null||e.scrollTarget!==void 0)&&(B.value=Ke(d.value,e.scrollTarget),te(B.value,h))}function ce(t){k!==!0?(Ee(b,t),n("click",t)):k=!1}function L(t){A.value===!0&&e.noFocus!==!0&&Me(i.value,t.target)!==!0&&x()}function C(t){n("escapeKey"),E(t)}function h(){const t=i.value;t===null||d.value===null||Oe({el:t,offset:e.offset,anchorEl:d.value,anchorOrigin:D.value,selfOrigin:se.value,absoluteOffset:m,fit:e.fit,cover:e.cover,maxHeight:e.maxHeight,maxWidth:e.maxWidth})}function de(){return y(me,Z.value,()=>c.value===!0?y("div",{role:"menu",...v,ref:i,tabindex:-1,class:["q-menu q-position-engine scroll"+ue.value,v.class],style:[v.style,ee.value],...ie.value},P(o.default)):null)}return ye(K),Object.assign(b,{focus:x,updatePosition:h}),ae}});let je=!1;{const e=document.createElement("div");e.setAttribute("dir","rtl"),Object.assign(e.style,{width:"1px",height:"1px",overflow:"auto"});const o=document.createElement("div");Object.assign(o.style,{width:"1000px",height:"1px"}),document.body.appendChild(e),e.appendChild(o),e.scrollLeft=-1e3,je=e.scrollLeft>=0,e.remove()}var Ye=S({name:"QCardSection",props:{tag:{type:String,default:"div"},horizontal:Boolean},setup(e,{slots:o}){const n=s(()=>`q-card__section q-card__section--${e.horizontal===!0?"horiz row no-wrap":"vert"}`);return()=>y(e.tag,{class:n.value},P(o.default))}}),Ze=S({name:"QCard",props:{...I,tag:{type:String,default:"div"},square:Boolean,flat:Boolean,bordered:Boolean},setup(e,{slots:o}){const{proxy:{$q:n}}=z(),v=U(e,n),a=s(()=>"q-card"+(v.value===!0?" q-card--dark q-dark":"")+(e.bordered===!0?" q-card--bordered":"")+(e.square===!0?" q-card--square no-border-radius":"")+(e.flat===!0?" q-card--flat no-shadow":""));return()=>y(e.tag,{class:a.value},P(o.default))}});export{Xe as Q,$ as a,We as b,_e as c,je as d,Ye as e,Ze as f,$e as r}; 2 | -------------------------------------------------------------------------------- /public/dashboard/assets/QCircularProgress.04fad857.js: -------------------------------------------------------------------------------- 1 | import{k as b,l as $}from"./QBtn.e5691bad.js";import{c as w,f as B}from"./render.bb221d47.js";import{b as _}from"./format.a33550d6.js";import{c as a,h as n,g as q}from"./index.02176f2b.js";const z={...b,min:{type:Number,default:0},max:{type:Number,default:100},color:String,centerColor:String,trackColor:String,fontSize:String,rounded:Boolean,thickness:{type:Number,default:.2,validator:e=>e>=0&&e<=1},angle:{type:Number,default:0},showValue:Boolean,reverse:Boolean,instantFeedback:Boolean},c=50,v=2*c,f=v*Math.PI,N=Math.round(f*1e3)/1e3;var D=w({name:"QCircularProgress",props:{...z,value:{type:Number,default:0},animationSpeed:{type:[String,Number],default:600},indeterminate:Boolean},setup(e,{slots:o}){const{proxy:{$q:u}}=q(),g=$(e),h=a(()=>{const r=(u.lang.rtl===!0?-1:1)*e.angle;return{transform:e.reverse!==(u.lang.rtl===!0)?`scale3d(-1, 1, 1) rotate3d(0, 0, 1, ${-90-r}deg)`:`rotate3d(0, 0, 1, ${r-90}deg)`}}),k=a(()=>e.instantFeedback!==!0&&e.indeterminate!==!0?{transition:`stroke-dashoffset ${e.animationSpeed}ms ease 0s, stroke ${e.animationSpeed}ms ease`}:""),t=a(()=>v/(1-e.thickness/2)),y=a(()=>`${t.value/2} ${t.value/2} ${t.value} ${t.value}`),i=a(()=>_(e.value,e.min,e.max)),C=a(()=>f*(1-(i.value-e.min)/(e.max-e.min))),s=a(()=>e.thickness/2*t.value);function d({thickness:r,offset:l,color:m,cls:S,rounded:x}){return n("circle",{class:"q-circular-progress__"+S+(m!==void 0?` text-${m}`:""),style:k.value,fill:"transparent",stroke:"currentColor","stroke-width":r,"stroke-dasharray":N,"stroke-dashoffset":l,"stroke-linecap":x,cx:t.value,cy:t.value,r:c})}return()=>{const r=[];e.centerColor!==void 0&&e.centerColor!=="transparent"&&r.push(n("circle",{class:`q-circular-progress__center text-${e.centerColor}`,fill:"currentColor",r:c-s.value/2,cx:t.value,cy:t.value})),e.trackColor!==void 0&&e.trackColor!=="transparent"&&r.push(d({cls:"track",thickness:s.value,offset:0,color:e.trackColor})),r.push(d({cls:"circle",thickness:s.value,offset:C.value,color:e.color,rounded:e.rounded===!0?"round":void 0}));const l=[n("svg",{class:"q-circular-progress__svg",style:h.value,viewBox:y.value,"aria-hidden":"true"},r)];return e.showValue===!0&&l.push(n("div",{class:"q-circular-progress__text absolute-full row flex-center content-center",style:{fontSize:e.fontSize}},o.default!==void 0?o.default():[n("div",i.value)])),n("div",{class:`q-circular-progress q-circular-progress--${e.indeterminate===!0?"in":""}determinate`,style:g.value,role:"progressbar","aria-valuemin":e.min,"aria-valuemax":e.max,"aria-valuenow":e.indeterminate===!0?void 0:i.value},B(o.internal,l))}}});export{D as Q}; 2 | -------------------------------------------------------------------------------- /public/dashboard/assets/QItem.8e917815.js: -------------------------------------------------------------------------------- 1 | import{c as d,h as v,a as S}from"./render.bb221d47.js";import{c as a,h as r,g as k,r as q,V as E,y as K}from"./index.02176f2b.js";import{u as R,b as $}from"./QBtn.e5691bad.js";var N=d({name:"QItemLabel",props:{overline:Boolean,caption:Boolean,header:Boolean,lines:[Number,String]},setup(e,{slots:l}){const n=a(()=>parseInt(e.lines,10)),i=a(()=>"q-item__label"+(e.overline===!0?" q-item__label--overline text-overline":"")+(e.caption===!0?" q-item__label--caption text-caption":"")+(e.header===!0?" q-item__label--header":"")+(n.value===1?" ellipsis":"")),u=a(()=>e.lines!==void 0&&n.value>1?{overflow:"hidden",display:"-webkit-box","-webkit-box-orient":"vertical","-webkit-line-clamp":n.value}:null);return()=>r("div",{style:u.value,class:i.value},v(l.default))}});const y={dark:{type:Boolean,default:null}};function g(e,l){return a(()=>e.dark===null?l.dark.isActive:e.dark)}var P=d({name:"QList",props:{...y,bordered:Boolean,dense:Boolean,separator:Boolean,padding:Boolean,tag:{type:String,default:"div"}},setup(e,{slots:l}){const n=k(),i=g(e,n.proxy.$q),u=a(()=>"q-list"+(e.bordered===!0?" q-list--bordered":"")+(e.dense===!0?" q-list--dense":"")+(e.separator===!0?" q-list--separator":"")+(i.value===!0?" q-list--dark":"")+(e.padding===!0?" q-list--padding":""));return()=>r(e.tag,{class:u.value},v(l.default))}}),F=d({name:"QItemSection",props:{avatar:Boolean,thumbnail:Boolean,side:Boolean,top:Boolean,noWrap:Boolean},setup(e,{slots:l}){const n=a(()=>`q-item__section column q-item__section--${e.avatar===!0||e.side===!0||e.thumbnail===!0?"side":"main"}`+(e.top===!0?" q-item__section--top justify-start":" justify-center")+(e.avatar===!0?" q-item__section--avatar":"")+(e.thumbnail===!0?" q-item__section--thumbnail":"")+(e.noWrap===!0?" q-item__section--nowrap":""));return()=>r("div",{class:n.value},v(l.default))}}),O=d({name:"QItem",props:{...y,...R,tag:{type:String,default:"div"},active:{type:Boolean,default:null},clickable:Boolean,dense:Boolean,insetLevel:Number,tabindex:[String,Number],focused:Boolean,manualFocus:Boolean},emits:["click","keyup"],setup(e,{slots:l,emit:n}){const{proxy:{$q:i}}=k(),u=g(e,i),{hasLink:m,linkAttrs:_,linkClass:h,linkTag:B,navigateOnClick:x}=$(),o=q(null),c=q(null),b=a(()=>e.clickable===!0||m.value===!0||e.tag==="label"),s=a(()=>e.disable!==!0&&b.value===!0),L=a(()=>"q-item q-item-type row no-wrap"+(e.dense===!0?" q-item--dense":"")+(u.value===!0?" q-item--dark":"")+(m.value===!0&&e.active===null?h.value:e.active===!0?` q-item--active${e.activeClass!==void 0?` ${e.activeClass}`:""}`:"")+(e.disable===!0?" disabled":"")+(s.value===!0?" q-item--clickable q-link cursor-pointer "+(e.manualFocus===!0?"q-manual-focusable":"q-focusable q-hoverable")+(e.focused===!0?" q-manual-focusable--focused":""):"")),C=a(()=>{if(e.insetLevel===void 0)return null;const t=i.lang.rtl===!0?"Right":"Left";return{["padding"+t]:16+e.insetLevel*56+"px"}});function Q(t){s.value===!0&&(c.value!==null&&(t.qKeyEvent!==!0&&document.activeElement===o.value?c.value.focus():document.activeElement===c.value&&o.value.focus()),x(t))}function w(t){if(s.value===!0&&E(t,13)===!0){K(t),t.qKeyEvent=!0;const f=new MouseEvent("click",t);f.qKeyEvent=!0,o.value.dispatchEvent(f)}n("keyup",t)}function I(){const t=S(l.default,[]);return s.value===!0&&t.unshift(r("div",{class:"q-focus-helper",tabindex:-1,ref:c})),t}return()=>{const t={ref:o,class:L.value,style:C.value,role:"listitem",onClick:Q,onKeyup:w};return s.value===!0?(t.tabindex=e.tabindex||"0",Object.assign(t,_.value)):b.value===!0&&(t["aria-disabled"]="true"),r(B.value,t,I())}}});export{F as Q,g as a,N as b,O as c,P as d,y as u}; 2 | -------------------------------------------------------------------------------- /public/dashboard/assets/QPage.edb823d8.js: -------------------------------------------------------------------------------- 1 | import{c as g,h}from"./render.bb221d47.js";import{i as r,j as t,c as s,h as p,l as d,C as f,g as y}from"./index.02176f2b.js";var Q=g({name:"QPage",props:{padding:Boolean,styleFn:Function},setup(a,{slots:i}){const{proxy:{$q:o}}=y(),e=r(d,t);if(e===t)return console.error("QPage needs to be a deep child of QLayout"),t;if(r(f,t)===t)return console.error("QPage needs to be child of QPageContainer"),t;const c=s(()=>{const n=(e.header.space===!0?e.header.size:0)+(e.footer.space===!0?e.footer.size:0);if(typeof a.styleFn=="function"){const l=e.isContainer.value===!0?e.containerHeight.value:o.screen.height;return a.styleFn(n,l)}return{minHeight:e.isContainer.value===!0?e.containerHeight.value-n+"px":o.screen.height===0?n!==0?`calc(100vh - ${n}px)`:"100vh":o.screen.height-n+"px"}}),u=s(()=>`q-page${a.padding===!0?" q-layout-padding":""}`);return()=>p("main",{class:u.value,style:c.value},h(i.default))}});export{Q}; 2 | -------------------------------------------------------------------------------- /public/dashboard/assets/QueueListPage.f55cfae4.js: -------------------------------------------------------------------------------- 1 | import{c as R,Q as w,b as V,d as B}from"./index.416c10b7.js";import{Q as u,b as m,c as g,d as U}from"./QItem.8e917815.js";import{h as A}from"./QPagination.6eb9fd1c.js";import{Q as f,R as I}from"./QBtn.e5691bad.js";import{Q as F}from"./QPage.edb823d8.js";import{S as r}from"./StatusCount.c6c9d9e0.js";import{H as S,I as l,J as i,K as a,d as e,M as _,N as C,P as q,r as v,w as $,o as z,k as D,Q as c,F as E,R as H,L as M,A as N}from"./index.02176f2b.js";import"./render.bb221d47.js";import"./axios.d530bc82.js";import"./format.a33550d6.js";import"./position-engine.2625858a.js";import"./selection.02425b78.js";const j={class:"text-weight-medium"},J={class:"text-grey-8 q-gutter-xs"},K=S({__name:"QueueListItem",props:{queue:null},setup(Q){const t=Q;return(o,d)=>(l(),i(g,{clickable:"",to:{path:"/queues/"+encodeURIComponent(t.queue.name)}},{default:a(()=>[e(u,{avatar:"",top:""},{default:a(()=>[e(f,{name:"queue",color:"black",size:"34px"})]),_:1}),e(u,{top:"",class:"col-2 gt-sm"},{default:a(()=>[e(m,{class:"q-mt-sm"},{default:a(()=>[_(C(t.queue.name),1)]),_:1})]),_:1}),e(u,{top:""},{default:a(()=>[e(m,{lines:"1"},{default:a(()=>[q("span",j,C(t.queue.name),1)]),_:1}),e(m,{caption:"",lines:"5"},{default:a(()=>[e(r,{count:t.queue.queued,label:"Queued",color:"primary"},null,8,["count"]),e(r,{count:t.queue.started,label:"Running",color:"info"},null,8,["count"]),e(r,{count:t.queue.cancelled,label:"Cancelled",color:"warning"},null,8,["count"]),e(r,{count:t.queue.failed,label:"Failed",color:"negative"},null,8,["count"]),e(r,{count:t.queue.succeeded,label:"Succeeded",color:"positive"},null,8,["count"])]),_:1})]),_:1}),e(u,{top:"",side:""},{default:a(()=>[q("div",J,[e(f,{class:"gt-xs",size:"12px",icon:"keyboard_arrow_right"})])]),_:1})]),_:1},8,["to"]))}}),T={key:0},G={class:"q-pa-lg flex flex-center"},O={key:1},W={key:2},ce=S({__name:"QueueListPage",setup(Q){const t=v(null),o=v(null),d=v(1);$(d,(n,s)=>{b()});function b(){o.value!==null&&o.value.stop(),o.value=R.queues.search().page(d.value).bypassAuth().listen().onUpdated(n=>t.value=n).start()}z(()=>b()),D(()=>{o.value!==null&&o.value.stop()});function P(n){var s;return(s=n.name)!=null?s:"default"}return(n,s)=>(l(),i(F,{class:"justify-evenly",padding:""},{default:a(()=>[e(V,null,{default:a(()=>[e(w,{icon:"list",to:"/queues",label:"Queues"}),e(w,{to:"/queues",label:"List Queues"})]),_:1}),e(U,{class:"rounded-borders q-pa-lg"},{default:a(()=>{var h,x,k,y,L;return[e(m,{header:""},{default:a(()=>[_("All Queues")]),_:1}),((h=t.value)==null?void 0:h.total)>0?(l(),c("div",T,[e(B),(l(!0),c(E,null,H((k=(x=t.value)==null?void 0:x.data)!=null?k:[],p=>(l(),c("div",{key:P(p)},[e(K,{queue:p},null,8,["queue"]),e(B)]))),128)),q("div",G,[((y=t.value)==null?void 0:y.total)>0?(l(),i(A,{key:0,input:"","model-value":t.value.current_page,"onUpdate:modelValue":s[0]||(s[0]=p=>d.value=p),max:t.value.last_page},null,8,["model-value","max"])):M("",!0)])])):((L=t.value)==null?void 0:L.total)===0?(l(),c("div",O,[N((l(),i(g,{clickable:""},{default:a(()=>[e(u,{avatar:""},{default:a(()=>[e(f,{color:"negative",name:"warning"})]),_:1}),e(u,null,{default:a(()=>[_("No queues found")]),_:1})]),_:1})),[[I]])])):(l(),c("div",W,[N((l(),i(g,{clickable:""},{default:a(()=>[e(u,{avatar:""},{default:a(()=>[e(f,{color:"primary",name:"sync"})]),_:1}),e(u,null,{default:a(()=>[_("Loading")]),_:1})]),_:1})),[[I]])]))]}),_:1})]),_:1}))}});export{ce as default}; 2 | -------------------------------------------------------------------------------- /public/dashboard/assets/QueueShowPage.e3e55b3a.js: -------------------------------------------------------------------------------- 1 | import{c as _,Q as d,b as f}from"./index.416c10b7.js";import{c as q,Q as b,b as m,d as Q}from"./QItem.8e917815.js";import{Q as p}from"./QPage.edb823d8.js";import{R as h}from"./QBtn.e5691bad.js";import{_ as v}from"./RunListWithFiltering.35641e08.js";import{H as y,r as w,o as g,k as B,I as o,J as l,K as e,d as t,P as a,A as k,M as u,N as x}from"./index.02176f2b.js";import"./render.bb221d47.js";import"./axios.d530bc82.js";import"./QPagination.6eb9fd1c.js";import"./format.a33550d6.js";import"./QCircularProgress.04fad857.js";import"./api.9fab9f7e.js";import"./dayjs.min.96389700.js";import"./relativeTime.c4fed282.js";const I={class:"row"},N={class:"col-12 col-sm-6 q-py-md"},j={class:"row"},L={class:"col-12"},$=y({__name:"QueueShowPage",props:{queue:null},setup(i){const s=i,r=w(null);return g(()=>{let n=_.queues.show(s.queue).bypassAuth().listen().onUpdated(c=>r.value=c).start();B(()=>{n.stop()})}),(n,c)=>r.value!==null?(o(),l(p,{key:0,class:"justify-evenly",padding:""},{default:e(()=>[t(f,null,{default:e(()=>[t(d,{icon:"list",to:"/jobs",label:"Jobs"}),t(d,{label:s.queue,icon:"view_stream",to:"/queues/"+s.queue},null,8,["label","to"])]),_:1}),a("div",I,[a("div",N,[t(Q,{bordered:"",separator:""},{default:e(()=>[k((o(),l(q,null,{default:e(()=>[t(b,null,{default:e(()=>[t(m,null,{default:e(()=>[u(x(s.queue),1)]),_:1}),t(m,{caption:""},{default:e(()=>[u("Queue name")]),_:1})]),_:1})]),_:1})),[[h]])]),_:1})])]),a("div",j,[a("div",L,[t(v,{title:"Runs in queue '"+i.queue+"'",queues:[s.queue]},null,8,["title","queues"])])])]),_:1})):(o(),l(p,{key:1,class:"items-center justify-evenly",padding:""},{default:e(()=>[u(" Loading ")]),_:1}))}});export{$ as default}; 2 | -------------------------------------------------------------------------------- /public/dashboard/assets/RunListWithFiltering.35641e08.js: -------------------------------------------------------------------------------- 1 | import{Q as m,b,c as w,d as P}from"./QItem.8e917815.js";import{c as U,d as z}from"./index.416c10b7.js";import{Q as _,h as T}from"./QPagination.6eb9fd1c.js";import{Q as f,R as B}from"./QBtn.e5691bad.js";import{Q as $}from"./QCircularProgress.04fad857.js";import{S as g}from"./api.9fab9f7e.js";import{d as N}from"./dayjs.min.96389700.js";import{r as D}from"./relativeTime.c4fed282.js";import{H as F,c as V,I as a,J as l,K as t,d as s,U as i,L as M,M as c,N as p,P as k,r as Q,w as x,o as j,k as E,Q as v,F as H,R as J,A}from"./index.02176f2b.js";const K={class:"text-weight-medium"},W={class:"text-grey-8 q-gutter-xs"},G=F({__name:"TrackedRunListItem",props:{trackedRun:null},setup(y){const e=y;N.extend(D);const h=V(()=>N().to(e.trackedRun.created_at)),u=V(()=>{let d=1,o=e.trackedRun;for(;o.parent!==null;)o=o.parent,d++;return d});return(d,o)=>(a(),l(w,{clickable:"",to:{path:"/run/"+e.trackedRun.id}},{default:t(()=>[s(m,{avatar:"",top:""},{default:t(()=>[e.trackedRun.status===i(g).Queued?(a(),l(f,{key:0,name:"hourglass_top",color:"black",size:"34px"})):e.trackedRun.status===i(g).Started?(a(),l($,{key:1,"show-value":"",value:e.trackedRun.percentage,rounded:"",size:"34px",class:"q-ma-md"},null,8,["value"])):e.trackedRun.status===i(g).Succeeded?(a(),l(f,{key:2,name:"done",color:"black",size:"34px"})):e.trackedRun.status===i(g).Failed?(a(),l(f,{key:3,name:"close",color:"black",size:"34px"})):e.trackedRun.status===i(g).Cancelled?(a(),l(f,{key:4,name:"not_interested",color:"black",size:"34px"})):M("",!0)]),_:1}),s(m,{top:"",class:"col-2 gt-sm"},{default:t(()=>[s(b,{class:"q-mt-sm"},{default:t(()=>[c(p(i(h)),1)]),_:1})]),_:1}),s(m,{top:""},{default:t(()=>[s(b,{lines:"1"},{default:t(()=>[k("span",K,p(e.trackedRun.status),1)]),_:1}),s(b,{caption:""},{default:t(()=>[k("span",null,[e.trackedRun.messages.length===0?(a(),l(_,{key:0,dense:"",icon:"chat"},{default:t(()=>[c("Messages: "+p(e.trackedRun.messages.length),1)]),_:1})):(a(),l(_,{key:1,dense:"",color:"blue","text-color":"white",icon:"chat"},{default:t(()=>[c(" Messages: "+p(e.trackedRun.messages.length),1)]),_:1}))]),k("span",null,[e.trackedRun.signals.length===0?(a(),l(_,{key:0,dense:"",icon:"sensors"},{default:t(()=>[c("Signals: "+p(e.trackedRun.signals.length),1)]),_:1})):(a(),l(_,{key:1,dense:"",color:"red","text-color":"white",icon:"sensors"},{default:t(()=>[c(" Signals: "+p(e.trackedRun.signals.length),1)]),_:1}))]),k("span",null,[i(u)===1?(a(),l(_,{key:0,dense:"",icon:"replay"},{default:t(()=>[c("Retries: "+p(i(u)-1),1)]),_:1})):(a(),l(_,{key:1,dense:"",color:"orange","text-color":"white",icon:"replay"},{default:t(()=>[c(" Retries: "+p(i(u)-1),1)]),_:1}))])]),_:1})]),_:1}),s(m,{top:"",side:""},{default:t(()=>[k("div",W,[s(f,{class:"gt-xs",size:"12px",icon:"keyboard_arrow_right"})])]),_:1})]),_:1},8,["to"]))}}),O={key:0},X={class:"q-pa-lg flex flex-center"},Y={key:1},Z={key:2},ce=F({__name:"RunListWithFiltering",props:{queues:null,aliases:null,batchIds:null,title:null},setup(y){const e=y,h=Q(1);x(h,(r,n)=>{o()});const u=Q(null),d=Q(null);x(()=>e.queues,(r,n)=>o()),x(()=>e.aliases,(r,n)=>o()),x(()=>e.batchIds,(r,n)=>o());function o(){d.value!==null&&d.value.stop();let r=U.runs.search();if(e.aliases!==void 0)for(let n of e.aliases)r=r.whereAlias(n);if(e.batchIds!==void 0)for(let n of e.batchIds)r=r.whereBatchId(n);if(e.queues!==void 0)for(let n of e.queues)r=r.whereQueue(n);d.value=r.page(h.value).bypassAuth().listen().onUpdated(n=>u.value=n).start()}return j(()=>o()),E(()=>{d.value!==null&&d.value.stop()}),(r,n)=>(a(),l(P,{bordered:"",class:"rounded-borders",style:{"min-width":"85%"}},{default:t(()=>{var q,I,S,L,C;return[s(b,{header:""},{default:t(()=>[c(p(y.title),1)]),_:1}),((q=u.value)==null?void 0:q.total)>0?(a(),v("div",O,[s(z),(a(!0),v(H,null,J((S=(I=u.value)==null?void 0:I.data)!=null?S:[],R=>(a(),v("div",{key:R.id},[s(G,{"tracked-run":R},null,8,["tracked-run"]),s(z)]))),128)),k("div",X,[((L=u.value)==null?void 0:L.total)>0?(a(),l(T,{key:0,input:"","model-value":u.value.current_page,"onUpdate:modelValue":n[0]||(n[0]=R=>h.value=R),max:u.value.last_page},null,8,["model-value","max"])):M("",!0)])])):((C=u.value)==null?void 0:C.total)===0?(a(),v("div",Y,[A((a(),l(w,{clickable:""},{default:t(()=>[s(m,{avatar:""},{default:t(()=>[s(f,{color:"negative",name:"warning"})]),_:1}),s(m,null,{default:t(()=>[c("No runs found")]),_:1})]),_:1})),[[B]])])):(a(),v("div",Z,[A((a(),l(w,{clickable:""},{default:t(()=>[s(m,{avatar:""},{default:t(()=>[s(f,{color:"primary",name:"sync"})]),_:1}),s(m,null,{default:t(()=>[c("Loading")]),_:1})]),_:1})),[[B]])]))]}),_:1}))}});export{ce as _}; 2 | -------------------------------------------------------------------------------- /public/dashboard/assets/StatusCount.c6c9d9e0.js: -------------------------------------------------------------------------------- 1 | import{c as W,e as re,h as se}from"./render.bb221d47.js";import{c as s,h as w,r as A,w as H,k as L,x as M,q as N,W as ue,g as ce,y as de,_ as fe,H as ge,I as S,Q as T,d as _,K as x,P as ve,M as p,N as C,F as me,R as he}from"./index.02176f2b.js";import{d as be,u as ye,v as Q,e as Se,a as Te,b as _e,f as xe,g as pe,c as Ce,r as D,s as we,p as V,h as Pe}from"./position-engine.2625858a.js";import{u as Oe,a as ke,b as qe,d as Be,c as j,g as Ee}from"./selection.02425b78.js";import{Q as Ae}from"./QPagination.6eb9fd1c.js";const He=["top","middle","bottom"];var Le=W({name:"QBadge",props:{color:String,textColor:String,floating:Boolean,transparent:Boolean,multiLine:Boolean,outline:Boolean,rounded:Boolean,label:[Number,String],align:{type:String,validator:e=>He.includes(e)}},setup(e,{slots:o}){const u=s(()=>e.align!==void 0?{verticalAlign:e.align}:null),r=s(()=>{const a=e.outline===!0&&e.color||e.textColor;return`q-badge flex inline items-center no-wrap q-badge--${e.multiLine===!0?"multi":"single"}-line`+(e.outline===!0?" q-badge--outline":e.color!==void 0?` bg-${e.color}`:"")+(a!==void 0?` text-${a}`:"")+(e.floating===!0?" q-badge--floating":"")+(e.rounded===!0?" q-badge--rounded":"")+(e.transparent===!0?" q-badge--transparent":"")});return()=>w("div",{class:r.value,style:u.value,role:"status","aria-label":e.label},re(o.default,e.label!==void 0?[e.label]:[]))}}),Me=W({name:"QTooltip",inheritAttrs:!1,props:{...be,...Oe,...ye,maxHeight:{type:String,default:null},maxWidth:{type:String,default:null},transitionShow:{default:"jump-down"},transitionHide:{default:"jump-up"},anchor:{type:String,default:"bottom middle",validator:Q},self:{type:String,default:"top middle",validator:Q},offset:{type:Array,default:()=>[14,14],validator:Se},scrollTarget:{default:void 0},delay:{type:Number,default:0},hideDelay:{type:Number,default:0}},emits:[...ke],setup(e,{slots:o,emit:u,attrs:r}){let a,c;const h=ce(),{proxy:{$q:l}}=h,d=A(null),v=A(!1),$=s(()=>V(e.anchor,l.lang.rtl)),I=s(()=>V(e.self,l.lang.rtl)),R=s(()=>e.persistent!==!0),{registerTick:F,removeTick:K}=Te(),{registerTimeout:m}=qe(),{transitionProps:U,transitionStyle:z}=_e(e),{localScrollTarget:P,changeScrollEvent:G,unconfigureScrollTarget:J}=xe(e,B),{anchorEl:n,canShow:X,anchorEvents:f}=pe({showing:v,configureAnchorEl:oe}),{show:Y,hide:b}=Be({showing:v,canShow:X,handleShow:ee,handleHide:te,hideOnRouteChange:R,processOnMount:!0});Object.assign(f,{delayShow:ae,delayHide:ne});const{showPortal:O,hidePortal:k,renderPortal:Z}=Ce(h,d,ie,"tooltip");if(l.platform.is.mobile===!0){const t={anchorEl:n,innerRef:d,onClickOutside(i){return b(i),i.target.classList.contains("q-dialog__backdrop")&&de(i),!0}},y=s(()=>e.modelValue===null&&e.persistent!==!0&&v.value===!0);H(y,i=>{(i===!0?Pe:D)(t)}),L(()=>{D(t)})}function ee(t){O(),F(()=>{c=new MutationObserver(()=>g()),c.observe(d.value,{attributes:!1,childList:!0,characterData:!0,subtree:!0}),g(),B()}),a===void 0&&(a=H(()=>l.screen.width+"|"+l.screen.height+"|"+e.self+"|"+e.anchor+"|"+l.lang.rtl,g)),m(()=>{O(!0),u("show",t)},e.transitionDuration)}function te(t){K(),k(),q(),m(()=>{k(!0),u("hide",t)},e.transitionDuration)}function q(){c!==void 0&&(c.disconnect(),c=void 0),a!==void 0&&(a(),a=void 0),J(),M(f,"tooltipTemp")}function g(){const t=d.value;n.value===null||!t||we({el:t,offset:e.offset,anchorEl:n.value,anchorOrigin:$.value,selfOrigin:I.value,maxHeight:e.maxHeight,maxWidth:e.maxWidth})}function ae(t){if(l.platform.is.mobile===!0){j(),document.body.classList.add("non-selectable");const y=n.value,i=["touchmove","touchcancel","touchend","click"].map(E=>[y,E,"delayHide","passiveCapture"]);N(f,"tooltipTemp",i)}m(()=>{Y(t)},e.delay)}function ne(t){l.platform.is.mobile===!0&&(M(f,"tooltipTemp"),j(),setTimeout(()=>{document.body.classList.remove("non-selectable")},10)),m(()=>{b(t)},e.hideDelay)}function oe(){if(e.noParentEvent===!0||n.value===null)return;const t=l.platform.is.mobile===!0?[[n.value,"touchstart","delayShow","passive"]]:[[n.value,"mouseenter","delayShow","passive"],[n.value,"mouseleave","delayHide","passive"]];N(f,"anchor",t)}function B(){if(n.value!==null||e.scrollTarget!==void 0){P.value=Ee(n.value,e.scrollTarget);const t=e.noParentEvent===!0?g:b;G(P.value,t)}}function le(){return v.value===!0?w("div",{...r,ref:d,class:["q-tooltip q-tooltip--style q-position-engine no-pointer-events",r.class],style:[r.style,z.value],role:"tooltip"},se(o.default)):null}function ie(){return w(ue,U.value,le)}return L(q),Object.assign(h.proxy,{updatePosition:g}),Z}});const Ne=ge({__name:"StatusCount",props:{count:null,label:null,color:null},setup(e){const o=e;return(u,r)=>(S(),T("span",null,[_(Ae,{dense:"",color:o.color,"text-color":"white"},{default:x(()=>[ve("span",null,[p(C(o.label)+" \xA0 ",1),(S(!0),T(me,null,he(o.count.toString().length,a=>(S(),T("span",{key:a}," \xA0 "))),128)),_(Le,{color:"orange",floating:""},{default:x(()=>[p(C(o.count),1)]),_:1})]),_(Me,null,{default:x(()=>[p(C(o.count),1)]),_:1})]),_:1},8,["color"])]))}});var $e=fe(Ne,[["__scopeId","data-v-48391fe4"]]);export{$e as S}; 2 | -------------------------------------------------------------------------------- /public/dashboard/assets/StatusCount.dafe58ca.css: -------------------------------------------------------------------------------- 1 | .spaced-avatar>div[data-v-48391fe4]{color:#ff0;overflow:visible!important} 2 | -------------------------------------------------------------------------------- /public/dashboard/assets/api.9fab9f7e.js: -------------------------------------------------------------------------------- 1 | var r=(e=>(e.Queued="queued",e.Started="started",e.Cancelled="cancelled",e.Failed="failed",e.Succeeded="succeeded",e.Released="released",e))(r||{}),d=(e=>(e.Success="success",e.Error="error",e.Info="info",e.Warning="warning",e.Debug="debug",e))(d||{});export{d as M,r as S}; 2 | -------------------------------------------------------------------------------- /public/dashboard/assets/axios.c0010d0a.js: -------------------------------------------------------------------------------- 1 | import{f as i}from"./index.02176f2b.js";import{a}from"./axios.d530bc82.js";const e=a.create({baseURL:"https://api.example.com"});var t=i(({app:o})=>{o.config.globalProperties.$axios=a,o.config.globalProperties.$api=e});export{e as api,t as default}; 2 | -------------------------------------------------------------------------------- /public/dashboard/assets/dayjs.min.96389700.js: -------------------------------------------------------------------------------- 1 | import{e as P}from"./index.416c10b7.js";var z={exports:{}};(function(V,Q){(function(A,T){V.exports=T()})(P,function(){var A=1e3,T=6e4,U=36e5,j="millisecond",p="second",S="minute",w="hour",M="day",b="week",l="month",F="quarter",v="year",O="date",J="Invalid Date",q=/^(\d{4})[-/]?(\d{1,2})?[-/]?(\d{0,2})[Tt\s]*(\d{1,2})?:?(\d{1,2})?:?(\d{1,2})?[.:]?(\d+)?$/,B=/\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,E={name:"en",weekdays:"Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"),months:"January_February_March_April_May_June_July_August_September_October_November_December".split("_"),ordinal:function(r){var e=["th","st","nd","rd"],t=r%100;return"["+r+(e[(t-20)%10]||e[t]||e[0])+"]"}},k=function(r,e,t){var i=String(r);return!i||i.length>=e?r:""+Array(e+1-i.length).join(t)+r},G={s:k,z:function(r){var e=-r.utcOffset(),t=Math.abs(e),i=Math.floor(t/60),n=t%60;return(e<=0?"+":"-")+k(i,2,"0")+":"+k(n,2,"0")},m:function r(e,t){if(e.date()1)return r(s[0])}else{var a=e.name;D[a]=e,n=a}return!i&&n&&(Y=n),n||!i&&Y},c=function(r,e){if(I(r))return r.clone();var t=typeof e=="object"?e:{};return t.date=r,t.args=arguments,new W(t)},o=G;o.l=L,o.i=I,o.w=function(r,e){return c(r,{locale:e.$L,utc:e.$u,x:e.$x,$offset:e.$offset})};var W=function(){function r(t){this.$L=L(t.locale,null,!0),this.parse(t)}var e=r.prototype;return e.parse=function(t){this.$d=function(i){var n=i.date,u=i.utc;if(n===null)return new Date(NaN);if(o.u(n))return new Date;if(n instanceof Date)return new Date(n);if(typeof n=="string"&&!/Z$/i.test(n)){var s=n.match(q);if(s){var a=s[2]-1||0,f=(s[7]||"0").substring(0,3);return u?new Date(Date.UTC(s[1],a,s[3]||1,s[4]||0,s[5]||0,s[6]||0,f)):new Date(s[1],a,s[3]||1,s[4]||0,s[5]||0,s[6]||0,f)}}return new Date(n)}(t),this.$x=t.x||{},this.init()},e.init=function(){var t=this.$d;this.$y=t.getFullYear(),this.$M=t.getMonth(),this.$D=t.getDate(),this.$W=t.getDay(),this.$H=t.getHours(),this.$m=t.getMinutes(),this.$s=t.getSeconds(),this.$ms=t.getMilliseconds()},e.$utils=function(){return o},e.isValid=function(){return this.$d.toString()!==J},e.isSame=function(t,i){var n=c(t);return this.startOf(i)<=n&&n<=this.endOf(i)},e.isAfter=function(t,i){return c(t)0,n<=o.r||!o.r){n<=1&&u>0&&(o=v[u-1]);var p=c[o.l];g&&(n=g(""+n)),s=typeof p=="string"?p.replace("%d",n):p(n,t,o.l,h);break}}if(t)return s;var M=h?c.future:c.past;return typeof M=="function"?M(s):M.replace("%s",s)},e.to=function(r,t){return T(r,t,this,!0)},e.from=function(r,t){return T(r,t,this)};var x=function(r){return r.$u?d.utc():d()};e.toNow=function(r){return this.to(x(this),r)},e.fromNow=function(r){return this.from(x(this),r)}}})})(b);var k=b.exports;export{k as r}; 2 | -------------------------------------------------------------------------------- /public/dashboard/assets/render.bb221d47.js: -------------------------------------------------------------------------------- 1 | import{X as o,H as f,h as u,A as v}from"./index.02176f2b.js";const h=n=>o(f(n)),s=n=>o(n);function m(n,e){return n!==void 0&&n()||e}function S(n,e){if(n!==void 0){const t=n();if(t!=null)return t.slice()}return e}function p(n,e){return n!==void 0?e.concat(n()):e}function l(n,e){return n===void 0?e:e!==void 0?e.concat(n()):n()}function D(n,e,t,a,r,c){e.key=a+r;const i=u(n,e,t);return r===!0?v(i,c()):i}export{S as a,s as b,h as c,D as d,p as e,l as f,m as h}; 2 | -------------------------------------------------------------------------------- /public/dashboard/assets/selection.02425b78.js: -------------------------------------------------------------------------------- 1 | import{v as x,g as H,c as b,d as L}from"./QBtn.e5691bad.js";import{w as y,o as M,g as V,z as T,$ as W,k as C,a0 as P}from"./index.02176f2b.js";const z={modelValue:{type:Boolean,default:null},"onUpdate:modelValue":[Function,Array]},D=["beforeShow","show","beforeHide","hide"];function N({showing:e,canShow:t,hideOnRouteChange:l,handleShow:n,handleHide:d,processOnMount:S}){const f=V(),{props:i,emit:s,proxy:m}=f;let u;function E(o){e.value===!0?c(o):p(o)}function p(o){if(i.disable===!0||o!==void 0&&o.qAnchorHandled===!0||t!==void 0&&t(o)!==!0)return;const r=i["onUpdate:modelValue"]!==void 0;r===!0&&(s("update:modelValue",!0),u=o,T(()=>{u===o&&(u=void 0)})),(i.modelValue===null||r===!1)&&w(o)}function w(o){e.value!==!0&&(e.value=!0,s("beforeShow",o),n!==void 0?n(o):s("show",o))}function c(o){if(i.disable===!0)return;const r=i["onUpdate:modelValue"]!==void 0;r===!0&&(s("update:modelValue",!1),u=o,T(()=>{u===o&&(u=void 0)})),(i.modelValue===null||r===!1)&&g(o)}function g(o){e.value!==!1&&(e.value=!1,s("beforeHide",o),d!==void 0?d(o):s("hide",o))}function v(o){i.disable===!0&&o===!0?i["onUpdate:modelValue"]!==void 0&&s("update:modelValue",!1):o===!0!==e.value&&(o===!0?w:g)(u)}y(()=>i.modelValue,v),l!==void 0&&x(f)===!0&&y(()=>m.$route.fullPath,()=>{l.value===!0&&e.value===!0&&c()}),S===!0&&M(()=>{v(i.modelValue)});const h={show:p,hide:c,toggle:E};return Object.assign(m,h),h}const U=[null,document,document.body,document.scrollingElement,document.documentElement];function O(e,t){let l=H(t);if(l===void 0){if(e==null)return window;l=e.closest(".scroll,.scroll-y,.overflow-auto")}return U.includes(l)?window:l}function k(e){return e===window?window.pageYOffset||window.scrollY||document.body.scrollTop||0:e.scrollTop}function B(e){return e===window?window.pageXOffset||window.scrollX||document.body.scrollLeft||0:e.scrollLeft}let a;function I(){if(a!==void 0)return a;const e=document.createElement("p"),t=document.createElement("div");b(e,{width:"100%",height:"200px"}),b(t,{position:"absolute",top:"0px",left:"0px",visibility:"hidden",width:"200px",height:"150px",overflow:"hidden"}),t.appendChild(e),document.body.appendChild(t);const l=e.offsetWidth;t.style.overflow="scroll";let n=e.offsetWidth;return l===n&&(n=t.clientWidth),t.remove(),a=l-n,a}function X(e,t=!0){return!e||e.nodeType!==Node.ELEMENT_NODE?!1:t?e.scrollHeight>e.clientHeight&&(e.classList.contains("scroll")||e.classList.contains("overflow-auto")||["auto","scroll"].includes(window.getComputedStyle(e)["overflow-y"])):e.scrollWidth>e.clientWidth&&(e.classList.contains("scroll")||e.classList.contains("overflow-auto")||["auto","scroll"].includes(window.getComputedStyle(e)["overflow-x"]))}function Y(){let e=null;const t=V();function l(){e!==null&&(clearTimeout(e),e=null)}return W(l),C(l),{removeTimeout:l,registerTimeout(n,d){l(),L(t)===!1&&(e=setTimeout(n,d))}}}function $(){if(window.getSelection!==void 0){const e=window.getSelection();e.empty!==void 0?e.empty():e.removeAllRanges!==void 0&&(e.removeAllRanges(),P.is.mobile!==!0&&e.addRange(document.createRange()))}else document.selection!==void 0&&document.selection.empty()}export{D as a,Y as b,$ as c,N as d,k as e,B as f,O as g,I as h,X as i,z as u}; 2 | -------------------------------------------------------------------------------- /public/dashboard/assets/use-prevent-scroll.cc490b6c.js: -------------------------------------------------------------------------------- 1 | import{k as T,Y as m,m as c,D as s,y as E,Z as S}from"./index.02176f2b.js";import{f as P,e as H,i as q}from"./selection.02425b78.js";function X(o,e,r){let t;function l(){t!==void 0&&(m.remove(t),t=void 0)}return T(()=>{o.value===!0&&l()}),{removeFromHistory:l,addToHistory(){t={condition:()=>r.value===!0,handler:e},m.add(t)}}}let d=0,p,v,a,w=!1,h,y,g,n=null;function x(o){C(o)&&E(o)}function C(o){if(o.target===document.body||o.target.classList.contains("q-layout__backdrop"))return!0;const e=S(o),r=o.shiftKey&&!o.deltaX,t=!r&&Math.abs(o.deltaX)<=Math.abs(o.deltaY),l=r||t?o.deltaY:o.deltaX;for(let f=0;f0&&i.scrollTop+i.clientHeight===i.scrollHeight:l<0&&i.scrollLeft===0?!0:l>0&&i.scrollLeft+i.clientWidth===i.scrollWidth}return!0}function b(o){o.target===document&&(document.scrollingElement.scrollTop=document.scrollingElement.scrollTop)}function u(o){w!==!0&&(w=!0,requestAnimationFrame(()=>{w=!1;const{height:e}=o.target,{clientHeight:r,scrollTop:t}=document.scrollingElement;(a===void 0||e!==window.innerHeight)&&(a=r-e,document.scrollingElement.scrollTop=t),t>a&&(document.scrollingElement.scrollTop-=Math.ceil((t-a)/8))}))}function L(o){const e=document.body,r=window.visualViewport!==void 0;if(o==="add"){const{overflowY:t,overflowX:l}=window.getComputedStyle(e);p=P(window),v=H(window),h=e.style.left,y=e.style.top,g=window.location.href,e.style.left=`-${p}px`,e.style.top=`-${v}px`,l!=="hidden"&&(l==="scroll"||e.scrollWidth>window.innerWidth)&&e.classList.add("q-body--force-scrollbar-x"),t!=="hidden"&&(t==="scroll"||e.scrollHeight>window.innerHeight)&&e.classList.add("q-body--force-scrollbar-y"),e.classList.add("q-body--prevent-scroll"),document.qScrollPrevented=!0,c.is.ios===!0&&(r===!0?(window.scrollTo(0,0),window.visualViewport.addEventListener("resize",u,s.passiveCapture),window.visualViewport.addEventListener("scroll",u,s.passiveCapture),window.scrollTo(0,0)):window.addEventListener("scroll",b,s.passiveCapture))}c.is.desktop===!0&&c.is.mac===!0&&window[`${o}EventListener`]("wheel",x,s.notPassive),o==="remove"&&(c.is.ios===!0&&(r===!0?(window.visualViewport.removeEventListener("resize",u,s.passiveCapture),window.visualViewport.removeEventListener("scroll",u,s.passiveCapture)):window.removeEventListener("scroll",b,s.passiveCapture)),e.classList.remove("q-body--prevent-scroll"),e.classList.remove("q-body--force-scrollbar-x"),e.classList.remove("q-body--force-scrollbar-y"),document.qScrollPrevented=!1,e.style.left=h,e.style.top=y,window.location.href===g&&window.scrollTo(p,v),a=void 0)}function V(o){let e="add";if(o===!0){if(d++,n!==null){clearTimeout(n),n=null;return}if(d>1)return}else{if(d===0||(d--,d>0))return;if(e="remove",c.is.ios===!0&&c.is.nativeMobile===!0){n!==null&&clearTimeout(n),n=setTimeout(()=>{L(e),n=null},100);return}}L(e)}function z(){let o;return{preventBodyScroll(e){e!==o&&(o!==void 0||e===!0)&&(o=e,V(e))}}}export{z as a,X as u}; 2 | -------------------------------------------------------------------------------- /public/dashboard/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobytwigger/laravel-job-status/8f6d4a0d609a6a46c3134a0cb171954afd4ee988/public/dashboard/favicon.ico -------------------------------------------------------------------------------- /public/dashboard/icons/favicon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobytwigger/laravel-job-status/8f6d4a0d609a6a46c3134a0cb171954afd4ee988/public/dashboard/icons/favicon-128x128.png -------------------------------------------------------------------------------- /public/dashboard/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobytwigger/laravel-job-status/8f6d4a0d609a6a46c3134a0cb171954afd4ee988/public/dashboard/icons/favicon-16x16.png -------------------------------------------------------------------------------- /public/dashboard/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobytwigger/laravel-job-status/8f6d4a0d609a6a46c3134a0cb171954afd4ee988/public/dashboard/icons/favicon-32x32.png -------------------------------------------------------------------------------- /public/dashboard/icons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tobytwigger/laravel-job-status/8f6d4a0d609a6a46c3134a0cb171954afd4ee988/public/dashboard/icons/favicon-96x96.png -------------------------------------------------------------------------------- /public/dashboard/index.html: -------------------------------------------------------------------------------- 1 | dashboardTitle 2 | 3 |
@jobapi -------------------------------------------------------------------------------- /routes/api.php: -------------------------------------------------------------------------------- 1 | group(function () { 6 | Route::get('batches', [\JobStatus\Http\Controllers\Api\BatchController::class, 'index']) 7 | ->name('batches.index'); 8 | Route::get('batches/{job_status_batch}', [\JobStatus\Http\Controllers\Api\BatchController::class, 'show']) 9 | ->name('batches.show'); 10 | 11 | Route::get('jobs', [\JobStatus\Http\Controllers\Api\JobController::class, 'index']) 12 | ->name('jobs.index'); 13 | Route::get('jobs/{job_status_job_alias}', [\JobStatus\Http\Controllers\Api\JobController::class, 'show']) 14 | ->name('jobs.show'); 15 | 16 | Route::get('queues', [\JobStatus\Http\Controllers\Api\QueueController::class, 'index']) 17 | ->name('queues.index'); 18 | Route::get('queues/{job_status_queue}', [\JobStatus\Http\Controllers\Api\QueueController::class, 'show']) 19 | ->name('queues.show'); 20 | 21 | Route::get('runs', [\JobStatus\Http\Controllers\Api\RunController::class, 'index']) 22 | ->name('runs.index'); 23 | Route::get('runs/{job_status_run}', [\JobStatus\Http\Controllers\Api\RunController::class, 'show']) 24 | ->name('runs.show'); 25 | Route::post('/runs/{job_status_run}/retry', [\JobStatus\Http\Controllers\Api\JobRetryController::class, 'store']) 26 | ->name('runs.retry'); 27 | Route::post('/runs/{job_status_run}/signal', [\JobStatus\Http\Controllers\Api\JobSignalController::class, 'store']) 28 | ->name('runs.signal'); 29 | }); 30 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | name('job-status.dashboard'); 5 | -------------------------------------------------------------------------------- /src/Concerns/InteractsWithSignals.php: -------------------------------------------------------------------------------- 1 | getJobStatus() 18 | ?->signals() 19 | ?->unhandled() 20 | ?->get() 21 | ?->each(fn (JobSignal $jobSignal) => $this->fireSignal($jobSignal)); 22 | } 23 | 24 | protected function fireSignal(JobSignal $signal) 25 | { 26 | $method = sprintf('on%s', Str::ucfirst(Str::camel($signal->signal))); 27 | if (method_exists($this, $method)) { 28 | $this->{$method}($signal->parameters ?? []); 29 | } elseif (method_exists($this, 'handleSignalCallback')) { 30 | $this->handleSignalCallback($signal->signal, $signal->parameters); 31 | } 32 | 33 | $signal->handled_at = Carbon::now(); 34 | $signal->save(); 35 | 36 | if ($signal->cancel_job === true) { 37 | throw JobCancelledException::for($signal); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Concerns/Trackable.php: -------------------------------------------------------------------------------- 1 | shouldTrack = $shouldTrack; 24 | } 25 | 26 | public function shouldTrack(): bool 27 | { 28 | return $this->shouldTrack && config('laravel-job-status.enabled', true); 29 | } 30 | 31 | public static function search(array $tags = []): Builder 32 | { 33 | $search = JobStatus::whereClass(static::class); 34 | foreach ($tags as $key => $value) { 35 | if (is_numeric($key)) { 36 | $search->whereTag($value); 37 | } else { 38 | $search->whereTag($key, $value); 39 | } 40 | } 41 | 42 | return $search; 43 | } 44 | 45 | public function history(): ?TrackedJob 46 | { 47 | return static::search($this->tags()) 48 | ->get()->jobs()->first(); 49 | } 50 | 51 | public function getJobStatus(): ?JobStatus 52 | { 53 | if (!isset($this->jobStatus)) { 54 | $this->jobStatus = null; 55 | if ($this->job?->getJobId()) { 56 | $this->jobStatus = app(JobStatusRepository::class)->getLatestByQueueReference($this->job->getJobId(), $this->job->getConnectionName()); 57 | } 58 | if ($this->jobStatus === null && $this->job?->uuid()) { 59 | $this->jobStatus = app(JobStatusRepository::class)->getLatestByUuid($this->job->uuid()); 60 | } 61 | } 62 | 63 | return $this->jobStatus; 64 | } 65 | 66 | public function status(): JobStatusModifier 67 | { 68 | if ($this->getJobStatus() === null && $this->shouldTrack()) { 69 | throw new \Exception('Could not get the status of the job'); 70 | } 71 | 72 | return new JobStatusModifier($this->getJobStatus()); 73 | } 74 | 75 | public function alias(): ?string 76 | { 77 | return get_class($this); 78 | } 79 | 80 | public function tags(): array 81 | { 82 | return []; 83 | } 84 | 85 | public function users(): array 86 | { 87 | return []; 88 | } 89 | 90 | public function isUnprotected(): bool 91 | { 92 | return true; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/Console/ClearJobStatusCommand.php: -------------------------------------------------------------------------------- 1 | option('preserve') ?? 0; 45 | 46 | $statuses = JobStatus::query(); 47 | if (!$this->option('all')) { 48 | if ($this->option('keep-failed')) { 49 | $statuses->whereStatusIn(Status::getFinishedUnfailedStatuses()); 50 | } else { 51 | $statuses->whereFinished(); 52 | } 53 | } 54 | 55 | if ($hours !== 0) { 56 | $statuses->where('updated_at', '<', now()->subHours($hours)); 57 | } 58 | 59 | $statuses = $statuses->pluck('id'); 60 | 61 | $this->withProgressBar($statuses, function (int $jobStatusId) { 62 | $jobStatus = JobStatus::find($jobStatusId); 63 | if ($jobStatus === null) { 64 | return; 65 | } 66 | if ($this->option('trim')) { 67 | $jobStatus->statuses()->delete(); 68 | $jobStatus->signals()->delete(); 69 | $jobStatus->messages()->delete(); 70 | } else { 71 | $jobStatus->delete(); 72 | } 73 | }); 74 | 75 | return self::SUCCESS; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Console/ShowJobStatusSummaryCommand.php: -------------------------------------------------------------------------------- 1 | option('class'), 47 | fn (Builder $query) => $query->where('class', $this->option('class')) 48 | ) 49 | ->when( 50 | $this->option('alias'), 51 | fn (Builder $query) => $query->where('alias', $this->option('alias')) 52 | ) 53 | ->orderBy('class') 54 | ->get(); 55 | 56 | $data = $statuses->jobs()->map(fn (TrackedJob $trackedJob) => array_merge( 57 | [ 58 | $trackedJob->jobClass(), 59 | ], 60 | collect(Status::cases())->filter(fn (Status $status) => $status !== Status::RELEASED)->map(fn (Status $enum) => $this->getStatusCount($trackedJob, $enum))->toArray() 61 | )); 62 | $this->table(array_merge( 63 | [ 64 | 'Job', 65 | ], 66 | collect(Status::cases())->filter(fn (Status $status) => $status !== Status::RELEASED)->map(fn (Status $enum) => Status::convertToHuman($enum))->toArray() 67 | ), $data); 68 | 69 | return static::SUCCESS; 70 | } 71 | 72 | private function getStatusCount(TrackedJob $trackedJob, Status $status): int 73 | { 74 | return $trackedJob->countWithStatus($status); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Dashboard/Commands/InstallAssets.php: -------------------------------------------------------------------------------- 1 | output(fn () => $this->line('Clearing old assets')); 34 | 35 | if ($assets->clear()) { 36 | $this->output(fn () => $this->info('Old assets cleared')); 37 | } else { 38 | $this->output(fn () => $this->warn('No assets need clearing')); 39 | } 40 | 41 | $this->output(fn () => $this->line('Installing assets')); 42 | 43 | $assets->publish(); 44 | 45 | $this->output(fn () => $this->info('Installed assets')); 46 | 47 | return Command::SUCCESS; 48 | } 49 | 50 | private function output(callable $write) 51 | { 52 | if (!$this->option('silent')) { 53 | $write(); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Dashboard/Http/Composers/DashboardVariables.php: -------------------------------------------------------------------------------- 1 | with('jobStatusVariables', [ 13 | 'path' => config('laravel-job-status.dashboard.path', 'job-status'), 14 | 'domain' => config('laravel-job-status.dashboard.domain', null), 15 | 'version' => app(InstalledVersion::class)->version(), 16 | 'assets_in_date' => app(Assets::class)->inDate(), 17 | ]); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Dashboard/Http/Controllers/DashboardController.php: -------------------------------------------------------------------------------- 1 | middleware(Authenticate::class); 13 | } 14 | 15 | public function __invoke() 16 | { 17 | return view('job-status::layout'); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Dashboard/Http/Middleware/Authenticate.php: -------------------------------------------------------------------------------- 1 | environment('local') 19 | || Gate::allows('viewJobStatus') 20 | ) 21 | ? $next($request) 22 | : abort(403); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Dashboard/Utils/Assets.php: -------------------------------------------------------------------------------- 1 | deleteDirectory($path); 18 | 19 | return true; 20 | } 21 | 22 | public function publish(): void 23 | { 24 | Artisan::call( 25 | VendorPublishCommand::class, 26 | ['--force' => true, '--tag' => 'job-status-dashboard'] 27 | ); 28 | } 29 | 30 | public function inDate(): bool 31 | { 32 | $publishedPath = public_path('vendor/job-status'); 33 | $localPath = __DIR__ . '/../../../public/dashboard'; 34 | 35 | return $this->getHashOfPath($publishedPath) === $this->getHashOfPath($localPath); 36 | } 37 | 38 | private function getFilesFromPath(string $path, string $basePath = null): array 39 | { 40 | $paths = []; 41 | $files = glob($path . '*', GLOB_MARK); 42 | foreach ($files as $file) { 43 | if (is_dir($file)) { 44 | $paths = array_merge($paths, $this->getFilesFromPath($file, $basePath ?? $path)); 45 | } else { 46 | $paths[] = Str::replace($basePath, '', $file); 47 | } 48 | } 49 | 50 | return $paths; 51 | } 52 | 53 | public function getHashOfPath(string $path): string 54 | { 55 | return implode(',', $this->getFilesFromPath($path)); 56 | } 57 | 58 | private function deleteDirectory(string $path): void 59 | { 60 | $files = glob($path . '*', GLOB_MARK); 61 | foreach ($files as $file) { 62 | if (is_dir($file)) { 63 | $this->deleteDirectory($file); 64 | } else { 65 | unlink($file); 66 | } 67 | } 68 | 69 | if (is_dir($path)) { 70 | rmdir($path); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Dashboard/Utils/InstalledVersion.php: -------------------------------------------------------------------------------- 1 | connections->connection($config['connection'] ?? null), 19 | $config['table'], 20 | $config['queue'], 21 | $config['retry_after'] ?? 60, 22 | $config['after_commit'] ?? null 23 | ); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/DatabaseQueueDecorator.php: -------------------------------------------------------------------------------- 1 | push($job, $data, $queue); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Enums/MessageType.php: -------------------------------------------------------------------------------- 1 | value); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Exceptions/CannotBeRetriedException.php: -------------------------------------------------------------------------------- 1 | signal) 13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Http/Controllers/Api/BatchController.php: -------------------------------------------------------------------------------- 1 | shouldBypassAuth()) { 17 | $query->forUsers($this->resolveAuth()); 18 | } 19 | 20 | return (new PaginateBatches()) 21 | ->paginate( 22 | $query, 23 | $request->input('page', 1), 24 | $request->input('per_page', 10), 25 | bypassAuth: $this->shouldBypassAuth(), 26 | userId: $this->resolveAuth() 27 | ); 28 | } 29 | 30 | public function show(JobBatch $batch) 31 | { 32 | $query = JobStatus::where('batch_id', $batch->id); 33 | if (!$this->shouldBypassAuth()) { 34 | $query->forUsers($this->resolveAuth()); 35 | } 36 | /** @var LengthAwarePaginator $result */ 37 | $result = $query->paginateBatches(1, 1); 38 | 39 | if ($result->count() === 0) { 40 | abort(403); 41 | } 42 | 43 | return $result->first(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Http/Controllers/Api/Controller.php: -------------------------------------------------------------------------------- 1 | Auth::user()?->getAuthIdentifier()); 17 | } 18 | 19 | protected function checkUserCanAccessJob(JobStatus $jobStatus) 20 | { 21 | $userId = $this->resolveAuth(); 22 | $jobRun = new JobRun($jobStatus); 23 | 24 | if (!$this->shouldBypassAuth() && !$jobRun->accessibleBy($userId)) { 25 | throw new AuthorizationException('You cannot access this job status', 403); 26 | } 27 | } 28 | 29 | public function shouldBypassAuth(): bool 30 | { 31 | if (request()->query('bypassAuth', false)) { 32 | if (Gate::allows('viewJobStatus')) { 33 | return true; 34 | } 35 | abort(403, 'You do not have permission to bypass auth'); 36 | } 37 | 38 | return false; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Http/Controllers/Api/JobController.php: -------------------------------------------------------------------------------- 1 | shouldBypassAuth()) { 16 | $query->forUsers($this->resolveAuth()); 17 | } 18 | 19 | return (new PaginateJobs()) 20 | ->paginate( 21 | $query, 22 | $request->input('page', 1), 23 | $request->input('per_page', 10) 24 | ); 25 | } 26 | 27 | public function show(string $jobStatusJobAlias) 28 | { 29 | $query = JobStatus::whereAlias($jobStatusJobAlias); 30 | if (!$this->shouldBypassAuth()) { 31 | $query->forUsers($this->resolveAuth()); 32 | } 33 | /** @var LengthAwarePaginator $result */ 34 | $result = $query->paginateJobs(1, 1); 35 | 36 | if ($result->count() === 0) { 37 | abort(404, 'No job runs found for alias: ' . $jobStatusJobAlias); 38 | } 39 | 40 | return $result->first(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Http/Controllers/Api/JobRetryController.php: -------------------------------------------------------------------------------- 1 | checkUserCanAccessJob($jobStatus); 15 | 16 | try { 17 | $jobStatus->toRun()->retry(); 18 | } catch (CannotBeRetriedException $e) { 19 | throw ValidationException::withMessages([$e->getMessage()]); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Http/Controllers/Api/JobSignalController.php: -------------------------------------------------------------------------------- 1 | checkUserCanAccessJob($jobStatus); 13 | 14 | $request->validate([ 15 | 'signal' => 'required|string|min:1', 16 | 'cancel_job' => 'required|boolean', 17 | 'parameters' => 'sometimes|array', 18 | ]); 19 | 20 | $jobStatus->signals()->create([ 21 | 'signal' => $request->input('signal'), 22 | 'cancel_job' => $request->input('cancel_job'), 23 | 'parameters' => $request->input('parameters', []), 24 | ]); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Http/Controllers/Api/QueueController.php: -------------------------------------------------------------------------------- 1 | shouldBypassAuth()) { 16 | $query->forUsers($this->resolveAuth()); 17 | } 18 | 19 | return (new PaginateQueues()) 20 | ->paginate( 21 | $query, 22 | $request->input('page', 1), 23 | $request->input('per_page', 10) 24 | ); 25 | } 26 | 27 | public function show(string $jobStatusQueue) 28 | { 29 | $query = JobStatus::whereQueue($jobStatusQueue); 30 | if (!$this->shouldBypassAuth()) { 31 | $query->forUsers($this->resolveAuth()); 32 | } 33 | /** @var LengthAwarePaginator $result */ 34 | $result = $query->paginateQueues(1, 1); 35 | 36 | if ($result->count() === 0) { 37 | abort(404, 'No job runs found in queue: ' . $jobStatusQueue); 38 | } 39 | 40 | return $result->first(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Http/Controllers/Api/RunController.php: -------------------------------------------------------------------------------- 1 | shouldBypassAuth()) { 17 | $query->forUsers($this->resolveAuth()); 18 | } 19 | 20 | if ($request->has('alias')) { 21 | $query->where(function (Builder $query) use ($request) { 22 | foreach ($request->input('alias') as $alias) { 23 | $query->orWhere('alias', $alias); 24 | } 25 | }); 26 | } 27 | 28 | if ($request->has('status')) { 29 | $query->where(function (Builder $query) use ($request) { 30 | foreach ($request->input('status') as $status) { 31 | $query->orWhere('status', $status); 32 | } 33 | }); 34 | } 35 | 36 | if ($request->has('queue')) { 37 | $query->where(function (Builder $query) use ($request) { 38 | foreach ($request->input('queue') as $queue) { 39 | $query->orWhere('queue', $queue); 40 | } 41 | }); 42 | } 43 | 44 | if ($request->has('batchId')) { 45 | $query->where(function (Builder $query) use ($request) { 46 | foreach ($request->input('batchId') as $batchId) { 47 | $query->orWhere('batch_id', $batchId); 48 | } 49 | }); 50 | } 51 | 52 | 53 | if ($request->has('tags')) { 54 | $query->whereTags( 55 | is_array($request->input('tags', [])) 56 | ? $request->input('tags', []) 57 | : json_decode($request->input('tags'), true) 58 | ); 59 | } 60 | 61 | return $query->paginateRuns( 62 | $request->input('page', 1), 63 | $request->input('per_page', 10) 64 | ); 65 | } 66 | 67 | public function show(JobStatus $jobStatus) 68 | { 69 | $this->checkUserCanAccessJob($jobStatus); 70 | 71 | if ($jobStatus->uuid) { 72 | // Load all the retries 73 | return JobStatus::query() 74 | ->forUsers($this->resolveAuth()) 75 | ->whereUuid($jobStatus->uuid) 76 | ->get() 77 | ->runs() 78 | ->first(); 79 | } 80 | 81 | return new JobRun($jobStatus); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Http/Requests/Api/Run/RunSearchRequest.php: -------------------------------------------------------------------------------- 1 | 'sometimes|array', 16 | 'tags' => ['sometimes', function ($attribute, $value, $fail) { 17 | if (!is_array($value)) { 18 | $value = json_decode($value, true); 19 | } 20 | if (!is_array($value)) { 21 | $fail('The tags must be an array.'); 22 | } 23 | }], 24 | 'status' => ['sometimes', 'array'], 25 | 'status.*' => [ 26 | 'string', 27 | Rule::in(Arr::map(Status::cases(), fn (Status $status) => $status->value)), 28 | ], 29 | 'batchId' => ['sometimes', 'array'], 30 | 'batchId.*' => [ 31 | 'numeric', sprintf('exists:%s_%s,id', config('laravel-job-status.table_prefix'), 'job_batches'), 32 | ], 33 | 'queue' => ['sometimes', 'array'], 34 | 'queue.*' => ['string'], 35 | ]; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Http/Requests/JobSignalStoreRequest.php: -------------------------------------------------------------------------------- 1 | 'required|string|min:1', 13 | 'cancel_job' => 'required|boolean', 14 | 'parameters' => 'sometimes|array', 15 | ]; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Http/Requests/JobStatusSearchRequest.php: -------------------------------------------------------------------------------- 1 | 'required|string', 13 | 'tags' => 'sometimes|array', 14 | ]; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/JobStatusRepository.php: -------------------------------------------------------------------------------- 1 | where('uuid', $uuid)->latest()->orderBy('id', 'DESC')->first(); 12 | } 13 | 14 | public function getLatestByQueueReference(string $jobId, string $connectionName): ?JobStatus 15 | { 16 | return JobStatus::query()->where('job_id', $jobId) 17 | ->where('connection_name', $connectionName) 18 | ->latest()->orderBy('id', 'DESC')->first(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/JobStatusServiceProvider.php: -------------------------------------------------------------------------------- 1 | commands([ 43 | ClearJobStatusCommand::class, 44 | ShowJobStatusSummaryCommand::class, 45 | InstallAssets::class, 46 | ]); 47 | } 48 | 49 | /** 50 | * Boot the translation services. 51 | * 52 | * - Allow assets to be published 53 | * 54 | * @throws \Illuminate\Contracts\Container\BindingResolutionException 55 | */ 56 | public function boot() 57 | { 58 | $this->publishAssets(); 59 | $this->mapRoutes(); 60 | $this->bindListeners(); 61 | $this->defineBladeDirective(); 62 | $this->setupGates(); 63 | $this->publishDashboardAssets(); 64 | $this->setupPaginationMethods(); 65 | } 66 | 67 | public function setupPaginationMethods() 68 | { 69 | foreach ([ 70 | 'paginateRuns' => PaginateRuns::class, 71 | 'paginateJobs' => PaginateJobs::class, 72 | 'paginateQueues' => PaginateQueues::class, 73 | 'paginateBatches' => PaginateBatches::class, 74 | ] as $name => $class) { 75 | \Illuminate\Database\Eloquent\Builder::macro( 76 | $name, 77 | fn () => app($class)->paginate($this, ...func_get_args()) 78 | ); 79 | } 80 | } 81 | 82 | /** 83 | * Publish any assets to allow the end user to customise the functionality of this package. 84 | */ 85 | private function publishAssets() 86 | { 87 | $this->mergeConfigFrom( 88 | __DIR__ . '/../config/laravel-job-status.php', 89 | 'laravel-job-status' 90 | ); 91 | 92 | $this->publishes([ 93 | __DIR__ . '/../config/laravel-job-status.php' => config_path('laravel-job-status.php'), 94 | ], ['config', 'laravel-job-status-config']); 95 | 96 | $this->publishes([ 97 | __DIR__ . '/../database/migrations/' => database_path('migrations'), 98 | ], 'migrations'); 99 | 100 | $this->loadMigrationsFrom(__DIR__ . '/../database/migrations'); 101 | } 102 | 103 | private function mapRoutes() 104 | { 105 | if (config('laravel-job-status.routes.api.enabled', true)) { 106 | Route::model('job_status_batch', JobBatch::class); 107 | Route::model('job_status_run', JobStatus::class); 108 | 109 | Route::prefix(config('laravel-job-status.routes.api.prefix')) 110 | ->middleware(config('laravel-job-status.routes.api.middleware', [])) 111 | ->name('api.job-status.') 112 | ->group(__DIR__ . '/../routes/api.php'); 113 | } 114 | 115 | if (config('laravel-job-status.dashboard.enabled', true)) { 116 | Route::prefix(config('laravel-job-status.dashboard.path', 'job-status')) 117 | ->domain(config('laravel-job-status.dashboard.domain', null)) 118 | ->middleware(config('laravel-job-status.dashboard.middleware', 'web')) 119 | ->group(function () { 120 | $this->loadRoutesFrom(__DIR__ . '/../routes/web.php'); 121 | }); 122 | } 123 | } 124 | 125 | protected function setupGates() 126 | { 127 | Gate::define('viewJobStatus', function () { 128 | return null; 129 | }); 130 | } 131 | 132 | private function bindListeners() 133 | { 134 | Event::listen(JobQueued::class, \JobStatus\Listeners\JobQueued::class); 135 | Event::listen(JobProcessing::class, \JobStatus\Listeners\JobProcessing::class); 136 | Event::listen(JobFailed::class, \JobStatus\Listeners\JobFailed::class); 137 | Event::listen(JobProcessed::class, \JobStatus\Listeners\JobProcessed::class); 138 | Event::listen(JobReleasedAfterException::class, \JobStatus\Listeners\JobReleasedAfterException::class); 139 | Event::listen(JobExceptionOccurred::class, \JobStatus\Listeners\JobExceptionOccurred::class); 140 | Event::listen(BatchDispatched::class, \JobStatus\Listeners\BatchDispatched::class); 141 | 142 | app()->booted(function () { 143 | Queue::addConnector('database', function () { 144 | return new DatabaseConnectorDecorator($this->app['db']); 145 | }); 146 | }); 147 | } 148 | 149 | private function defineBladeDirective() 150 | { 151 | if ($this->app->resolved('blade.compiler')) { 152 | $this->defineJobStatusBladeDirective($this->app['blade.compiler']); 153 | } else { 154 | $this->app->afterResolving('blade.compiler', function (BladeCompiler $bladeCompiler) { 155 | $this->defineJobStatusBladeDirective($bladeCompiler); 156 | }); 157 | } 158 | } 159 | 160 | private function defineJobStatusBladeDirective(BladeCompiler $compiler) 161 | { 162 | $compiler->directive('jobapi', function () { 163 | return '%s", app(\JobStatus\Share\ShareConfig::class)->toString()); ?>'; 164 | }); 165 | } 166 | 167 | private function publishDashboardAssets() 168 | { 169 | $this->loadViewsFrom(resource_path('views/vendor/job-status'), 'job-status'); 170 | 171 | $this->publishes([ 172 | __DIR__ . '/../public/dashboard' => public_path('vendor/job-status'), 173 | ], ['job-status-dashboard']); 174 | $this->publishes([ 175 | __DIR__ . '/../public/dashboard/index.html' => resource_path('views/vendor/job-status/layout.blade.php'), 176 | ], ['job-status-dashboard']); 177 | 178 | View::composer('job-status::layout', DashboardVariables::class); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/Listeners/BatchDispatched.php: -------------------------------------------------------------------------------- 1 | $event->batch->id], 13 | ['name' => $event->batch->name] 14 | ); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Listeners/JobExceptionOccurred.php: -------------------------------------------------------------------------------- 1 | job); 23 | 24 | if (Helper::isTrackingEnabled()) { 25 | // If the job is a cancelled job, we want to make sure the job doesn't run again. For this, we need to actually fail the job! 26 | if ($event->exception instanceof JobCancelledException) { 27 | $helper->getJob()->fail($event->exception); 28 | } 29 | $modifier = $helper->getJobStatusModifier(); 30 | if ($modifier === null) { 31 | return true; 32 | } 33 | 34 | // This is only the case if JobFailed has not ran, so we need to update the status since Jobfailed hasn't done it. 35 | if ($modifier->getJobStatus()->status !== Status::FAILED && $modifier->getJobStatus()->status !== Status::CANCELLED) { 36 | $modifier->setFinishedAt(now()); 37 | if ($event->exception instanceof JobCancelledException) { 38 | $modifier->setStatus(Status::CANCELLED); 39 | } else { 40 | $modifier->setStatus(Status::FAILED); 41 | $modifier->addException($event->exception); 42 | } 43 | } 44 | 45 | // Happens if the job has been released already during the job, and therefore won't be once job failed 46 | if ($helper->getJob()->isReleased()) { 47 | $jobStatus = JobStatus::create([ 48 | 'queue' => $modifier->getJobStatus()->queue, 49 | 'class' => $modifier->getJobStatus()?->class, 50 | 'alias' => $modifier->getJobStatus()?->alias, 51 | 'percentage' => 0, 52 | 'batch_id' => $modifier->getJobStatus()->batch_id, 53 | 'status' => Status::QUEUED, 54 | 'uuid' => $helper->getJob()->uuid(), 55 | 'connection_name' => $helper->getJob()->getConnectionName(), 56 | 'is_unprotected' => $modifier->getJobStatus()?->is_unprotected, 57 | ]); 58 | 59 | 60 | JobStatusModifier::forJobStatus($jobStatus)->setStatus(Status::QUEUED); 61 | 62 | foreach ($modifier->getJobStatus()->tags()->get() as $tag) { 63 | $jobStatus->tags()->create([ 64 | 'key' => $tag->key, 65 | 'value' => $tag->value, 66 | 'is_indexless' => $tag->is_indexless, 67 | ]); 68 | } 69 | 70 | foreach ($modifier->getJobStatus()->users()->get() as $user) { 71 | $jobStatus->users()->create([ 72 | 'user_id' => $user->user_id, 73 | ]); 74 | } 75 | } 76 | 77 | $modifier->setPercentage(100); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Listeners/JobFailed.php: -------------------------------------------------------------------------------- 1 | job); 28 | 29 | if (Helper::isTrackingEnabled()) { 30 | $modifier = $helper->getJobStatusModifier(); 31 | if ($modifier === null) { 32 | return true; 33 | } 34 | 35 | $modifier->setPercentage(100); 36 | 37 | // This is only the case if JobExceptionOccurred has not been ran 38 | if ($modifier->getJobStatus()->status !== Status::FAILED && $modifier->getJobStatus()->status !== Status::CANCELLED) { 39 | $modifier->setFinishedAt(now()); 40 | 41 | if ($event->exception instanceof JobCancelledException) { 42 | $modifier->setStatus(Status::CANCELLED); 43 | } else { 44 | $modifier->setStatus(Status::FAILED); 45 | $modifier->addException($event->exception); 46 | } 47 | } 48 | } 49 | } 50 | 51 | private function batchIsCancelled(?\JobStatus\Models\JobStatus $jobStatus): bool 52 | { 53 | if ($jobStatus === null) { 54 | return false; 55 | } 56 | $batchId = $jobStatus->batch?->batch_id; 57 | if ($batchId !== null) { 58 | /** @var Batch|null $batch */ 59 | $batch = app(BatchRepository::class)->find($batchId); 60 | 61 | return $batch?->cancelled() ?? false; 62 | } 63 | 64 | return false; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Listeners/JobProcessed.php: -------------------------------------------------------------------------------- 1 | job); 29 | 30 | if (Helper::isTrackingEnabled()) { 31 | $modifier = $helper->getJobStatusModifier(); 32 | if ($modifier === null) { 33 | return; 34 | } 35 | 36 | if ($modifier->getJobStatus()->status === Status::STARTED) { 37 | $modifier->setFinishedAt(now()); 38 | $modifier->setPercentage(100.0); 39 | 40 | if ($helper->getJob()->hasFailed()) { 41 | $modifier->setStatus(Status::FAILED); 42 | } elseif ($helper->getJob()->isReleased()) { 43 | $modifier->setStatus(Status::RELEASED); 44 | } elseif ($this->batchIsCancelled($modifier->getJobStatus())) { 45 | $modifier->setStatus(Status::CANCELLED); 46 | $modifier->warningMessage('The batch that the job is a part of has been cancelled'); 47 | } else { 48 | $modifier->setStatus(Status::SUCCEEDED); 49 | } 50 | } 51 | 52 | if ($helper->getJob()->isReleased()) { 53 | $jobStatus = JobStatus::create([ 54 | 'class' => $modifier->getJobStatus()?->class, 55 | 'alias' => $modifier->getJobStatus()?->alias, 56 | 'queue' => $modifier->getJobStatus()->queue, 57 | 'batch_id' => $modifier->getJobStatus()->batch_id, 58 | 'percentage' => 0, 59 | 'status' => Status::QUEUED, 60 | 'uuid' => $helper->getJob()->uuid(), 61 | 'connection_name' => $helper->getJob()->getConnectionName(), 62 | 'job_id' => $helper->getJob()->getJobId(), 63 | 'is_unprotected' => $modifier->getJobStatus()?->is_unprotected, 64 | ]); 65 | 66 | $newModifier = JobStatusModifier::forJobStatus($jobStatus)->setStatus(Status::QUEUED); 67 | 68 | foreach ($modifier->getJobStatus()->tags()->get() as $tag) { 69 | $jobStatus->tags()->create([ 70 | 'key' => $tag->key, 71 | 'value' => $tag->value, 72 | 'is_indexless' => $tag->is_indexless, 73 | ]); 74 | } 75 | 76 | foreach ($modifier->getJobStatus()?->users()->get() as $user) { 77 | $newModifier->grantAccessTo($user->user_id); 78 | } 79 | } 80 | 81 | $modifier->setPercentage(100); 82 | } 83 | } 84 | 85 | private function batchIsCancelled(?JobStatus $jobStatus): bool 86 | { 87 | if ($jobStatus === null) { 88 | return false; 89 | } 90 | $batchId = $jobStatus->batch?->batch_id; 91 | if ($batchId !== null) { 92 | /** @var Batch|null $batch */ 93 | $batch = app(BatchRepository::class)->find($batchId); 94 | 95 | return $batch?->cancelled() ?? false; 96 | } 97 | 98 | return false; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/Listeners/JobProcessing.php: -------------------------------------------------------------------------------- 1 | job); 23 | 24 | if (Helper::isTrackingEnabled()) { 25 | $modifier = $helper->getJobStatusModifier(); 26 | 27 | if ($modifier !== null) { 28 | $modifier->setStatus(Status::STARTED); 29 | $modifier->setStartedAt(now()); 30 | 31 | $batchId = $modifier->getJobStatus()?->batch?->batch_id; 32 | if ($batchId !== null) { 33 | /** @var Batch|null $batch */ 34 | $batch = app(BatchRepository::class)->find($batchId); 35 | if ($batch?->cancelled()) { 36 | if ($modifier !== null) { 37 | $modifier->setFinishedAt(now()); 38 | $modifier->setPercentage(100.0); 39 | $modifier->setStatus(Status::CANCELLED); 40 | $modifier->warningMessage('The batch that the job is a part of has been cancelled'); 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Listeners/JobQueued.php: -------------------------------------------------------------------------------- 1 | job; 31 | 32 | if ($job instanceof CallQueuedListener) { 33 | $job = app($job->displayName()); 34 | } 35 | if ($this->validateJob($job) === false) { 36 | return true; 37 | } 38 | 39 | if (method_exists($job, 'batch')) { 40 | $batchModel = ($job->batch() !== null 41 | ? JobBatch::firstOrCreate( 42 | ['batch_id' => $job->batch()->id], 43 | ['name' => $job->batch()->name] 44 | ) 45 | : null); 46 | } else { 47 | $batchModel = null; 48 | } 49 | 50 | $jobStatus = JobStatus::create([ 51 | 'class' => get_class($job), 52 | 'alias' => method_exists($job, 'alias') ? $job->alias() : get_class($job), 53 | 'percentage' => 0, 54 | 'queue' => (property_exists($job, 'job') && $job?->job && $job->job?->getQueue()) 55 | ? $job->job->getQueue() 56 | : $job?->queue ?? $job?->queue ?? config(sprintf('queue.connections.%s.queue', $event->connectionName), null), 57 | 'payload' => ((property_exists($job, 'job') && $job?->job && $job->job?->getQueue()) 58 | ? $job->job?->payload() 59 | : null), 60 | 'batch_id' => $batchModel?->id, 61 | 'status' => Status::QUEUED, 62 | 'uuid' => null, 63 | 'job_id' => $event->id, 64 | 'connection_name' => $event->connectionName, 65 | 'is_unprotected' => method_exists($job, 'isUnprotected') ? $job->isUnprotected() : true, 66 | ]); 67 | 68 | $modifier = JobStatusModifier::forJobStatus($jobStatus); 69 | $modifier->setStatus(Status::QUEUED); 70 | 71 | foreach ((method_exists($job, 'users') ? $job->users() : []) as $user) { 72 | $modifier->grantAccessTo($user); 73 | } 74 | 75 | foreach ((method_exists($job, 'tags') ? $job->tags() : []) as $key => $value) { 76 | if (is_numeric($key)) { 77 | $jobStatus->tags()->create([ 78 | 'is_indexless' => true, 79 | 'key' => $value, 80 | 'value' => null, 81 | ]); 82 | } else { 83 | $jobStatus->tags()->create([ 84 | 'is_indexless' => false, 85 | 'key' => $key, 86 | 'value' => $value, 87 | ]); 88 | } 89 | } 90 | 91 | if (property_exists($job, 'job') && $job->job instanceof \Illuminate\Contracts\Queue\Job) { 92 | $this->checkJobUpToDate($modifier, $job->job); 93 | } 94 | } 95 | } 96 | 97 | protected function validateJob(mixed $job): bool 98 | { 99 | if (is_string($job) || $job instanceof \Closure) { 100 | return false; 101 | } 102 | if (!is_object($job)) { 103 | return false; 104 | } 105 | 106 | // True if extends Illuminate\Contracts\Queue\Job 107 | if (method_exists($job, 'resolveName')) { 108 | if (!$job->resolveName() || !class_exists($job->resolveName())) { 109 | return false; 110 | } 111 | if (!in_array(Trackable::class, class_uses_recursive($job->resolveName()))) { 112 | return config('laravel-job-status.track_anonymous', false); 113 | } 114 | } else { 115 | if (!in_array(Trackable::class, class_uses_recursive($job))) { 116 | return config('laravel-job-status.track_anonymous', false); 117 | } 118 | } 119 | 120 | return true; 121 | } 122 | 123 | protected function checkJobUpToDate(JobStatusModifier $jobStatusModifier, JobContract $job): void 124 | { 125 | if ($job->uuid() !== null && $jobStatusModifier->getJobStatus()->uuid !== $job->uuid()) { 126 | $jobStatusModifier->setUuid($job->uuid()); 127 | } 128 | if ($job->getJobId() !== null && $jobStatusModifier->getJobStatus()->job_id !== $job->getJobId()) { 129 | $jobStatusModifier->setJobId($job->getJobId()); 130 | } 131 | 132 | if ($jobStatusModifier->getJobStatus()->payload === null) { 133 | $jobStatusModifier->setPayload($job->payload()); 134 | } 135 | 136 | if ($job->getQueue() !== null && $jobStatusModifier->getJobStatus()->queue !== $job->getQueue()) { 137 | $jobStatusModifier->setQueue($job->getQueue()); 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/Listeners/JobReleasedAfterException.php: -------------------------------------------------------------------------------- 1 | job); 23 | 24 | if (Helper::isTrackingEnabled()) { 25 | $modifier = $helper->getJobStatusModifier(); 26 | if ($modifier === null) { 27 | return; 28 | } 29 | 30 | $jobStatus = JobStatus::create([ 31 | 'queue' => $modifier->getJobStatus()->queue, 32 | 'class' => $modifier->getJobStatus()?->class, 33 | 'alias' => $modifier->getJobStatus()?->alias, 34 | 'percentage' => 0, 35 | 'batch_id' => $modifier->getJobStatus()->batch_id, 36 | 'status' => Status::QUEUED, 37 | 'uuid' => $helper->getJob()->uuid(), 38 | 'connection_name' => $helper->getJob()->getConnectionName(), 39 | 'job_id' => $helper->getJob()->getJobId(), 40 | 'is_unprotected' => $modifier->getJobStatus()?->is_unprotected, 41 | ]); 42 | 43 | 44 | JobStatusModifier::forJobStatus($jobStatus)->setStatus(Status::QUEUED); 45 | 46 | foreach ($modifier->getJobStatus()->tags()->get() as $tag) { 47 | $jobStatus->tags()->create([ 48 | 'key' => $tag->key, 49 | 'value' => $tag->value, 50 | 'is_indexless' => $tag->is_indexless, 51 | ]); 52 | } 53 | 54 | foreach ($modifier->getJobStatus()->users()->get() as $user) { 55 | $jobStatus->users()->create([ 56 | 'user_id' => $user->user_id, 57 | ]); 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Listeners/Utils/Helper.php: -------------------------------------------------------------------------------- 1 | job = $job; 22 | } 23 | 24 | public static function forJob(Job $job) 25 | { 26 | return new static($job); 27 | } 28 | 29 | public static function isTrackingEnabled(): bool 30 | { 31 | return config('laravel-job-status.enabled', true); 32 | } 33 | 34 | public function getJob(): Job 35 | { 36 | return $this->job; 37 | } 38 | 39 | public function getTrackable(): null|object|string 40 | { 41 | $job = null; 42 | 43 | if ($this->job instanceof \Illuminate\Queue\Jobs\Job) { 44 | $job = $this->job->resolveName(); 45 | } 46 | 47 | return $job; 48 | } 49 | 50 | protected function checkJobUpToDate(JobStatusModifier $jobStatusModifier, Job $job): void 51 | { 52 | if ($job->uuid() !== null && $jobStatusModifier->getJobStatus()->uuid !== $job->uuid()) { 53 | $jobStatusModifier->setUuid($job->uuid()); 54 | } 55 | if ($job->getJobId() !== null && $jobStatusModifier->getJobStatus()->job_id !== $job->getJobId()) { 56 | $jobStatusModifier->setJobId($job->getJobId()); 57 | } 58 | 59 | if ($jobStatusModifier->getJobStatus()->payload === null) { 60 | $jobStatusModifier->setPayload($job->payload()); 61 | } 62 | 63 | if ($job->getQueue() !== null && $jobStatusModifier->getJobStatus()->queue !== $job->getQueue()) { 64 | $jobStatusModifier->setQueue($job->getQueue()); 65 | } 66 | } 67 | 68 | protected function isTrackable(): bool 69 | { 70 | if (!in_array(Trackable::class, class_uses_recursive($this->getTrackable()))) { 71 | return config('laravel-job-status.track_anonymous', false); 72 | } 73 | 74 | return true; 75 | } 76 | 77 | protected function getJobStatus(Job $job): ?JobStatus 78 | { 79 | $jobStatus = null; 80 | 81 | // Try to get the job status by job id and connection name, as this is the most reliable way. 82 | if ($job->getJobId()) { 83 | $jobStatus = app(JobStatusRepository::class)->getLatestByQueueReference($job->getJobId(), $job->getConnectionName()); 84 | } 85 | if ($jobStatus === null && $job->uuid()) { 86 | $jobStatus = app(JobStatusRepository::class)->getLatestByUuid($job->uuid()); 87 | } 88 | if ($jobStatus === null && $job->getConnectionName() === 'sync') { 89 | if (str_starts_with($job->payload()['data']['command'], 'O:')) { 90 | $command = unserialize($job->payload()['data']['command']); 91 | } elseif (App::bound(Encrypter::class)) { 92 | $command = unserialize(App::make(Encrypter::class)->decrypt($job->payload()['data']['command'])); 93 | } else { 94 | throw new \RuntimeException('Unable to extract job payload.'); 95 | } 96 | $batchId = null; 97 | if (method_exists($command, 'batch') && $command->batch() !== null) { 98 | $batchId = JobBatch::firstOrCreate( 99 | ['batch_id' => $command->batch()->id], 100 | ['name' => $command->batch()->name] 101 | )->id; 102 | } 103 | $jobStatus = JobStatus::create([ 104 | 'class' => method_exists($command, 'displayName') ? $command->displayName() : get_class($command), 105 | 'alias' => method_exists($command, 'alias') ? $command->alias() : get_class($command), 106 | 'queue' => $job->getQueue(), 107 | 'payload' => $job->payload(), 108 | 'percentage' => 0, 109 | 'status' => Status::QUEUED, 110 | 'uuid' => $job->uuid(), 111 | 'batch_id' => $batchId, 112 | 'connection_name' => $job->getConnectionName(), 113 | 'job_id' => $job->getJobId(), 114 | 'is_unprotected' => method_exists($command, 'isUnprotected') ? $command->isUnprotected() : true, 115 | ]); 116 | $modifier = JobStatusModifier::forJobStatus($jobStatus)->setStatus(Status::QUEUED); 117 | foreach ((method_exists($command, 'tags') ? $command->tags() : []) as $key => $value) { 118 | if (is_numeric($key)) { 119 | $jobStatus->tags()->create([ 120 | 'is_indexless' => true, 121 | 'key' => $value, 122 | 'value' => null, 123 | ]); 124 | } else { 125 | $jobStatus->tags()->create([ 126 | 'is_indexless' => false, 127 | 'key' => $key, 128 | 'value' => $value, 129 | ]); 130 | } 131 | } 132 | 133 | foreach ((method_exists($command, 'users') ? $command->users() : []) as $user) { 134 | $modifier->grantAccessTo($user); 135 | } 136 | } 137 | 138 | return $jobStatus; 139 | } 140 | 141 | public function getJobStatusModifier(): ?JobStatusModifier 142 | { 143 | if ($this->isTrackable() === false) { 144 | return null; 145 | } 146 | 147 | $jobStatus = $this->getJobStatus($this->job); 148 | 149 | if ($jobStatus === null) { 150 | return null; 151 | } 152 | $modifier = new JobStatusModifier($jobStatus); 153 | $this->checkJobUpToDate($modifier, $this->job); 154 | 155 | return $modifier; 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /src/Models/JobBatch.php: -------------------------------------------------------------------------------- 1 | table = sprintf('%s_%s', config('laravel-job-status.table_prefix'), 'job_batches'); 23 | parent::__construct($attributes); 24 | } 25 | 26 | public function jobStatus() 27 | { 28 | return $this->hasMany(JobStatus::class, 'batch_id'); 29 | } 30 | 31 | protected static function newFactory() 32 | { 33 | return JobBatchFactory::new(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Models/JobException.php: -------------------------------------------------------------------------------- 1 | 'array', 25 | 'line' => 'integer', 26 | 'code' => 'integer', 27 | ]; 28 | 29 | protected $with = [ 30 | 'previous', 31 | ]; 32 | 33 | protected $dateFormat = 'Y-m-d H:i:s.v'; 34 | 35 | public function __construct(array $attributes = []) 36 | { 37 | $this->table = sprintf('%s_%s', config('laravel-job-status.table_prefix'), 'job_exceptions'); 38 | parent::__construct($attributes); 39 | } 40 | 41 | public function jobStatus() 42 | { 43 | return $this->hasOne(JobStatus::class, 'exception_id'); 44 | } 45 | 46 | public function previous() 47 | { 48 | return $this->belongsTo(JobException::class, 'previous_id'); 49 | } 50 | 51 | public function loadAllPrevious(): static 52 | { 53 | $currentException = $this; 54 | $count = 1; 55 | $this->load('previous'); 56 | while ($currentException->previous_id !== null) { 57 | $string = ''; 58 | for ($i = 0; $i<=$count;$i++) { 59 | $string .= '.previous.previous'; 60 | } 61 | if (Str::startsWith($string, '.')) { 62 | $string = Str::substr($string, 1); 63 | } 64 | if (!$this->relationLoaded($string)) { 65 | $this->load($string); 66 | } 67 | $currentException = $currentException->previous; 68 | } 69 | 70 | return $this; 71 | } 72 | 73 | protected static function newFactory() 74 | { 75 | return JobExceptionFactory::new(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Models/JobMessage.php: -------------------------------------------------------------------------------- 1 | MessageType::class, 24 | ]; 25 | 26 | protected $dateFormat = 'Y-m-d H:i:s.v'; 27 | 28 | public function __construct(array $attributes = []) 29 | { 30 | $this->table = sprintf('%s_%s', config('laravel-job-status.table_prefix'), 'job_messages'); 31 | parent::__construct($attributes); 32 | } 33 | 34 | protected static function booted() 35 | { 36 | static::creating(fn (JobMessage $jobMessage) => config('laravel-job-status.collectors.messages.enabled', true)); 37 | } 38 | 39 | public function jobStatus() 40 | { 41 | return $this->belongsTo(JobStatus::class); 42 | } 43 | 44 | protected static function newFactory() 45 | { 46 | return JobMessageFactory::new(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Models/JobSignal.php: -------------------------------------------------------------------------------- 1 | 'array', 23 | 'cancel_job' => 'boolean', 24 | 'handled_at' => 'datetime', 25 | ]; 26 | 27 | protected $dateFormat = 'Y-m-d H:i:s.v'; 28 | 29 | public function __construct(array $attributes = []) 30 | { 31 | $this->table = sprintf('%s_%s', config('laravel-job-status.table_prefix'), 'job_signals'); 32 | parent::__construct($attributes); 33 | } 34 | 35 | protected static function booted() 36 | { 37 | static::creating(fn (JobSignal $jobSignal) => config('laravel-job-status.collectors.signals.enabled', true)); 38 | } 39 | 40 | public static function scopeUnhandled(Builder $query) 41 | { 42 | $query->whereNull('handled_at'); 43 | } 44 | 45 | public function jobStatus() 46 | { 47 | return $this->belongsTo(JobStatus::class); 48 | } 49 | 50 | protected static function newFactory() 51 | { 52 | return JobSignalFactory::new(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Models/JobStatusStatus.php: -------------------------------------------------------------------------------- 1 | Status::class, 24 | ]; 25 | 26 | protected $dateFormat = 'Y-m-d H:i:s.v'; 27 | 28 | public function __construct(array $attributes = []) 29 | { 30 | $this->table = sprintf('%s_%s', config('laravel-job-status.table_prefix'), 'job_status_statuses'); 31 | parent::__construct($attributes); 32 | } 33 | 34 | protected static function booted() 35 | { 36 | static::creating(fn (JobStatusStatus $jobStatus) => config('laravel-job-status.collectors.status_history.enabled', true)); 37 | } 38 | 39 | public function jobStatus() 40 | { 41 | return $this->belongsTo(JobStatus::class); 42 | } 43 | 44 | protected static function newFactory() 45 | { 46 | return JobStatusStatusFactory::new(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Models/JobStatusTag.php: -------------------------------------------------------------------------------- 1 | 'boolean', 23 | ]; 24 | 25 | public function __construct(array $attributes = []) 26 | { 27 | $this->table = sprintf('%s_%s', config('laravel-job-status.table_prefix'), 'job_status_tags'); 28 | parent::__construct($attributes); 29 | } 30 | 31 | public function jobStatus() 32 | { 33 | return $this->belongsTo(JobStatus::class); 34 | } 35 | 36 | protected static function newFactory() 37 | { 38 | return JobStatusTagFactory::new(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Models/JobStatusUser.php: -------------------------------------------------------------------------------- 1 | table = sprintf('%s_%s', config('laravel-job-status.table_prefix'), 'job_status_users'); 26 | parent::__construct($attributes); 27 | } 28 | 29 | public function jobStatus() 30 | { 31 | return $this->belongsTo(JobStatus::class); 32 | } 33 | 34 | protected static function newFactory() 35 | { 36 | return new JobStatusUserFactory(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Retry/JobRetrier.php: -------------------------------------------------------------------------------- 1 | jobStatus = $jobStatus; 20 | } 21 | 22 | public function retry(): void 23 | { 24 | if (!empty($this->emptyRequiredFields())) { 25 | throw new CannotBeRetriedException('All the following fields must be given: ' . implode(', ', $this->emptyRequiredFields())); 26 | } 27 | 28 | $jobId = Queue::connection($this->jobStatus->connection_name)->pushRaw( 29 | $this->preparePayloadForRefresh(), 30 | $this->jobStatus->queue 31 | ); 32 | 33 | if ($jobId === null) { 34 | throw CannotBeRetriedException::reason('The queue must return an ID when job pushed, none returned. The driver you are using is probably not supported.'); 35 | } 36 | 37 | $retryJobStatus = JobStatus::create([ 38 | 'class' => $this->jobStatus->class, 39 | 'alias' => $this->jobStatus->alias, 40 | 'percentage' => 0, 41 | 'status' => Status::QUEUED, 42 | 'uuid' => $this->jobStatus->uuid, 43 | 'job_id' => $jobId, 44 | 'connection_name' => $this->jobStatus->connection_name, 45 | 'exception_id' => null, 46 | 'started_at' => null, 47 | 'finished_at' => null, 48 | 'is_unprotected' => $this->jobStatus->is_unprotected, 49 | 'batch_id' => $this->jobStatus->batch_id, 50 | 'queue' => $this->jobStatus->queue, 51 | 'payload' => $this->jobStatus->payload, 52 | ]); 53 | 54 | $modifier = new JobStatusModifier($retryJobStatus); 55 | 56 | $modifier->setStatus(Status::QUEUED); 57 | foreach ($this->jobStatus->tags as $tag) { 58 | $retryJobStatus->tags()->create([ 59 | 'is_indexless' => $tag->is_indexless, 60 | 'key' => $tag->key, 61 | 'value' => $tag->value, 62 | ]); 63 | } 64 | 65 | foreach ($this->jobStatus->users as $user) { 66 | $modifier->grantAccessTo($user->user_id); 67 | } 68 | } 69 | 70 | private function preparePayloadForRefresh(): string 71 | { 72 | $payload = $this->jobStatus->payload; 73 | 74 | if (! isset($payload['data']['command'])) { 75 | return json_encode($payload); 76 | } 77 | 78 | if (str_starts_with($payload['data']['command'], 'O:')) { 79 | $instance = unserialize($payload['data']['command']); 80 | } elseif (App::bound(Encrypter::class)) { 81 | $instance = unserialize(App::make(Encrypter::class)->decrypt($payload['data']['command'])); 82 | } 83 | 84 | if (! isset($instance)) { 85 | throw new RuntimeException('Unable to extract job payload for refresh.'); 86 | } 87 | 88 | if (is_object($instance) && ! $instance instanceof \__PHP_Incomplete_Class && method_exists($instance, 'retryUntil')) { 89 | $retryUntil = $instance->retryUntil(); 90 | 91 | $payload['retryUntil'] = $retryUntil instanceof \DateTimeInterface 92 | ? $retryUntil->getTimestamp() 93 | : $retryUntil; 94 | } 95 | 96 | if (isset($payload['attempts'])) { 97 | $payload['attempts'] = 0; 98 | } 99 | 100 | return json_encode($payload); 101 | } 102 | 103 | private function emptyRequiredFields(): array 104 | { 105 | $fields = []; 106 | if ($this->jobStatus->connection_name === null) { 107 | $fields[] = 'Connection name'; 108 | } 109 | if ($this->jobStatus->queue === null) { 110 | $fields[] = 'Queue'; 111 | } 112 | if ($this->jobStatus->payload === null) { 113 | $fields[] = 'Payload'; 114 | } 115 | if (in_array($this->jobStatus->connection_name, ['sync'])) { 116 | $fields[] = 'Unsupported driver'; 117 | } 118 | 119 | return $fields; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Retry/JobRetrierFactory.php: -------------------------------------------------------------------------------- 1 | sortByDesc(fn (JobRun $jobRun) => $jobRun->jobStatus()->created_at) 13 | ->first(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Search/Collections/JobStatusCollection.php: -------------------------------------------------------------------------------- 1 | transform($this); 16 | } 17 | 18 | public function jobs(): TrackedJobCollection 19 | { 20 | return (new JobsTransformer())->transform($this); 21 | } 22 | 23 | public function queues(): QueueCollection 24 | { 25 | return (new QueuesTransformer())->transform($this); 26 | } 27 | 28 | public function batches(): BatchCollection 29 | { 30 | return (new BatchesTransformer())->transform($this); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Search/Collections/QueueCollection.php: -------------------------------------------------------------------------------- 1 | select(['batch_id']) 14 | ->selectRaw('max(created_at) as created_at') 15 | ->selectRaw('max(id) as id') 16 | ->withoutEagerLoads() 17 | ->groupBy('batch_id') 18 | ->orderBy('created_at', 'desc') 19 | ->orderBy('id', 'desc'); 20 | 21 | $results = $query->paginate(perPage: $perPage, page: $page); 22 | 23 | return new LengthAwarePaginator( 24 | (new JobStatusCollection($results->items()))->batches(), 25 | $results->total(), 26 | $perPage, 27 | $page 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Search/Queries/PaginateJobs.php: -------------------------------------------------------------------------------- 1 | select(['alias']) 14 | ->selectRaw('max(created_at) as created_at') 15 | ->selectRaw('max(id) as id') 16 | ->withoutEagerLoads() 17 | ->groupBy('alias') 18 | ->orderBy('created_at', 'desc') 19 | ->orderBy('id', 'desc'); 20 | 21 | $results = $query->paginate(perPage: $perPage, page: $page); 22 | 23 | return new LengthAwarePaginator( 24 | (new JobStatusCollection($results->items()))->jobs(), 25 | $results->total(), 26 | $perPage, 27 | $page 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Search/Queries/PaginateQueues.php: -------------------------------------------------------------------------------- 1 | select(['queue']) 14 | ->selectRaw('max(created_at) as created_at') 15 | ->selectRaw('max(id) as id') 16 | ->withoutEagerLoads() 17 | ->groupBy('queue') 18 | ->orderBy('created_at', 'desc') 19 | ->orderBy('id', 'desc'); 20 | 21 | $results = $query->paginate(perPage: $perPage, page: $page); 22 | 23 | return new LengthAwarePaginator( 24 | (new JobStatusCollection($results->items()))->queues(), 25 | $results->total(), 26 | $perPage, 27 | $page 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Search/Queries/PaginateRuns.php: -------------------------------------------------------------------------------- 1 | select(['selector']) 14 | ->selectRaw('max(created_at) as created_at') 15 | ->selectRaw('max(id) as id') 16 | ->withoutEagerLoads() 17 | ->groupBy('selector') 18 | ->orderBy('created_at', 'desc') 19 | ->orderBy('id', 'desc'); 20 | 21 | $results = $query->paginate(perPage: $perPage, page: $page); 22 | 23 | return new LengthAwarePaginator( 24 | (new JobStatusCollection($results->items()))->runs(), 25 | $results->total(), 26 | $perPage, 27 | $page 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Search/Result/Batch.php: -------------------------------------------------------------------------------- 1 | batch = $batch; 22 | $this->numberOfRuns = $numberOfRuns; 23 | $this->countWithStatus = $countWithStatus; 24 | } 25 | 26 | public function batchId(): string 27 | { 28 | return $this->batch->batch_id; 29 | } 30 | 31 | public function name(): ?string 32 | { 33 | return $this->batch->name; 34 | } 35 | 36 | public function toArray() 37 | { 38 | return [ 39 | 'count' => $this->numberOfRuns(), 40 | 'name' => $this->name(), 41 | 'batch_id' => $this->batchId(), 42 | 'queued' => $this->countWithStatus(Status::QUEUED), 43 | 'started' => $this->countWithStatus(Status::STARTED), 44 | 'failed' => $this->countWithStatus(Status::FAILED), 45 | 'succeeded' => $this->countWithStatus(Status::SUCCEEDED), 46 | 'cancelled' => $this->countWithStatus(Status::CANCELLED), 47 | 'created_at' => $this->batch->created_at, 48 | 'id' => $this->batch->id, 49 | ]; 50 | } 51 | 52 | public function numberOfRuns(): int 53 | { 54 | return $this->numberOfRuns ?? 0; 55 | } 56 | 57 | public function countWithStatus(Status $status): int 58 | { 59 | return array_key_exists($status->value, $this->countWithStatus) 60 | ? $this->countWithStatus[$status->value] 61 | : 0; 62 | } 63 | 64 | public function toJson($options = 0) 65 | { 66 | return json_encode($this->toArray(), $options); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Search/Result/JobRun.php: -------------------------------------------------------------------------------- 1 | jobStatus = $jobStatus; 34 | $this->parent = $parent; 35 | $this->releasedRuns = $releasedRuns ?? new JobRunCollection(); 36 | } 37 | 38 | public function getTagsAsArray(): array 39 | { 40 | $tags = []; 41 | foreach ($this->jobStatus->tags as $tag) { 42 | if ($tag->is_indexless) { 43 | $tags[] = $tag->key; 44 | } else { 45 | $tags[$tag->key] = $tag->value; 46 | } 47 | } 48 | 49 | return $tags; 50 | } 51 | 52 | 53 | public function hasParent(): bool 54 | { 55 | return $this->parent() !== null; 56 | } 57 | 58 | /** 59 | * @return JobRun 60 | */ 61 | public function parent(): ?JobRun 62 | { 63 | return $this->parent; 64 | } 65 | 66 | public function toArray() 67 | { 68 | return [ 69 | 'alias' => $this->jobStatus->alias, 70 | 'class' => $this->jobStatus->class, 71 | 'percentage' => $this->jobStatus->percentage, 72 | 'status' => $this->jobStatus->status, 73 | 'uuid' => $this->jobStatus->uuid, 74 | 'has_parent' => $this->hasParent(), 75 | 'parent' => $this->parent()?->toArray(), 76 | 'tags' => $this->getTagsAsArray(), 77 | 'created_at' => $this->jobStatus->created_at, 78 | 'exception' => $this->getException()?->toArray(), 79 | 'messages' => $this->jobStatus->messages->sortBy([['created_at', 'desc'], ['id', 'desc']]) 80 | ->map(fn (JobMessage $message) => $message->toArray())->values(), 81 | 'signals' => $this->jobStatus->signals->sortBy([['created_at', 'desc'], ['id', 'desc']]) 82 | ->map(fn (JobSignal $signal) => $signal->toArray())->values(), 83 | 'started_at' => $this->jobStatus->started_at, 84 | 'finished_at' => $this->jobStatus->finished_at, 85 | 'id' => $this->jobStatus->id, 86 | 'batch_id' => $this->jobStatus->batch_id, 87 | 'batch_id_uuid' => $this->jobStatus->batch?->batch_id, 88 | 'statuses' => $this->jobStatus->statuses->sortBy([['created_at', 'desc'], ['id', 'desc']]) 89 | ->map(fn (JobStatusStatus $status) => $status->toArray())->values(), 90 | 'has_payload' => $this->jobStatus->payload !== null, 91 | 'connection_name' => $this->jobStatus->connection_name, 92 | 'queue' => $this->jobStatus->queue, 93 | 'released_runs' => $this->releasedRuns->toArray(), 94 | ]; 95 | } 96 | 97 | public function getException(): ?JobException 98 | { 99 | return $this->jobStatus 100 | ->exception 101 | ?->loadAllPrevious(); 102 | } 103 | 104 | public function toJson($options = 0) 105 | { 106 | return collect($this->toArray())->toJson($options); 107 | } 108 | 109 | public function jobStatus(): JobStatus 110 | { 111 | return $this->jobStatus; 112 | } 113 | 114 | public function isARetry(): bool 115 | { 116 | return $this->hasParent(); 117 | } 118 | 119 | public function signals(): Collection 120 | { 121 | return $this->jobStatus->signals; 122 | } 123 | 124 | public function messagesOfType(MessageType $type) 125 | { 126 | return $this->jobStatus->messages() 127 | ->where('type', $type) 128 | ->latest() 129 | ->pluck('message'); 130 | } 131 | 132 | public function mostRecentMessage(bool $includeDebug = false): ?string 133 | { 134 | return $this->jobStatus->messages() 135 | ->when($includeDebug === false, fn (Builder $query) => $query->where('type', '!=', MessageType::DEBUG)) 136 | ->latest() 137 | ->orderBy('id', 'DESC') 138 | ->first() 139 | ?->message; 140 | } 141 | 142 | public function messages(): Collection 143 | { 144 | return $this->jobStatus->messages; 145 | } 146 | 147 | public function isFinished(): bool 148 | { 149 | return in_array($this->jobStatus->status, Status::getFinishedStatuses()); 150 | } 151 | 152 | public function isSuccessful(): bool 153 | { 154 | return $this->jobStatus->status === Status::SUCCEEDED; 155 | } 156 | 157 | public function isFailed(): bool 158 | { 159 | return $this->jobStatus->status === Status::FAILED; 160 | } 161 | 162 | public function isCancelled(): bool 163 | { 164 | return $this->jobStatus->status === Status::CANCELLED; 165 | } 166 | 167 | public function isQueued(): bool 168 | { 169 | return $this->jobStatus->status === Status::QUEUED; 170 | } 171 | 172 | public function isRunning(): bool 173 | { 174 | return $this->jobStatus->status === Status::STARTED; 175 | } 176 | 177 | public function getPercentage(): float 178 | { 179 | return $this->jobStatus->percentage; 180 | } 181 | 182 | public function getStatus(): Status 183 | { 184 | return $this->jobStatus->status; 185 | } 186 | 187 | public function accessibleBy(?int $userId): bool 188 | { 189 | return $this->trackingIsUnprotected() 190 | || $this->jobStatus->users()->where('user_id', $userId)->exists(); 191 | } 192 | 193 | public function trackingIsUnprotected(): bool 194 | { 195 | return $this->jobStatus->is_unprotected; 196 | } 197 | 198 | public function modifier(): JobStatusModifier 199 | { 200 | return new JobStatusModifier($this->jobStatus()); 201 | } 202 | 203 | public function retry() 204 | { 205 | Retrier::for($this->jobStatus)->retry(); 206 | } 207 | 208 | public function releasedRuns(): JobRunCollection 209 | { 210 | return $this->releasedRuns; 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/Search/Result/Queue.php: -------------------------------------------------------------------------------- 1 | queueName = $queueName; 23 | $this->numberOfRuns = $numberOfRuns; 24 | $this->countWithStatus = $countWithStatus; 25 | } 26 | 27 | public function name(): ?string 28 | { 29 | return $this->queueName; 30 | } 31 | 32 | public function toArray() 33 | { 34 | return [ 35 | 'count' => $this->numberOfRuns(), 36 | 'name' => $this->name(), 37 | 'queued' => $this->countWithStatus(Status::QUEUED), 38 | 'started' => $this->countWithStatus(Status::STARTED), 39 | 'failed' => $this->countWithStatus(Status::FAILED), 40 | 'succeeded' => $this->countWithStatus(Status::SUCCEEDED), 41 | 'cancelled' => $this->countWithStatus(Status::CANCELLED), 42 | ]; 43 | } 44 | 45 | public function numberOfRuns(): int 46 | { 47 | return $this->numberOfRuns ?? 0; 48 | } 49 | 50 | public function countWithStatus(Status $status): int 51 | { 52 | return array_key_exists($status->value, $this->countWithStatus) 53 | ? $this->countWithStatus[$status->value] 54 | : 0; 55 | } 56 | 57 | public function toJson($options = 0) 58 | { 59 | return json_encode($this->toArray(), $options); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Search/Result/TrackedJob.php: -------------------------------------------------------------------------------- 1 | jobClass; 21 | } 22 | 23 | public function __construct( 24 | string $jobClass, 25 | string $alias, 26 | ?int $numberOfRuns = null, 27 | array $failureReasons = [], 28 | array $countWithStatus = [] 29 | ) { 30 | $this->jobClass = $jobClass; 31 | $this->alias = $alias; 32 | $this->numberOfRuns = $numberOfRuns; 33 | $this->failureReasons = $failureReasons; 34 | $this->countWithStatus = $countWithStatus; 35 | } 36 | 37 | public function toArray() 38 | { 39 | return [ 40 | 'count' => $this->numberOfRuns(), 41 | 'alias' => $this->alias, 42 | 'class' => $this->jobClass, 43 | 'failure_reasons' => $this->getFailureReasons(), 44 | 'successful' => $this->countWithStatus(Status::SUCCEEDED), 45 | 'failed' => $this->countWithStatus(Status::FAILED), 46 | 'started' => $this->countWithStatus(Status::STARTED), 47 | 'queued' => $this->countWithStatus(Status::QUEUED), 48 | 'cancelled' => $this->countWithStatus(Status::CANCELLED), 49 | ]; 50 | } 51 | 52 | public function getFailureReasons(): array 53 | { 54 | return $this->failureReasons; 55 | } 56 | 57 | public function toJson($options = 0) 58 | { 59 | return json_encode($this->toArray(), $options); 60 | } 61 | 62 | public function alias(): string 63 | { 64 | return $this->alias; 65 | } 66 | 67 | public function numberOfRuns(): int 68 | { 69 | return $this->numberOfRuns ?? 0; 70 | } 71 | 72 | public function countWithStatus(Status $status): int 73 | { 74 | return array_key_exists($status->value, $this->countWithStatus) 75 | ? $this->countWithStatus[$status->value] 76 | : 0; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Search/Transformers/BatchesTransformer.php: -------------------------------------------------------------------------------- 1 | groupBy('batch_id') 15 | ->keys(); 16 | 17 | $queryResult = JobStatus::query() 18 | ->select('batch_id') 19 | ->selectRaw('MAX(class) as class') 20 | ->selectRaw('MAX(created_at) as created_at') 21 | ->selectRaw('MAX(id) as id') 22 | ->selectRaw('COUNT(DISTINCT selector) as count') 23 | ->whereIn('batch_id', $batchIds) 24 | ->with('batch') 25 | ->groupBy('batch_id') 26 | ->orderBy('created_at', 'desc') 27 | ->orderBy('id', 'desc') 28 | ->get(); 29 | 30 | $batchCollection = new BatchCollection(); 31 | foreach ($queryResult as $jobResults) { 32 | // Improve this to not make any additional queries. 33 | $batchCollection->push( 34 | new Batch( 35 | $jobResults->batch, 36 | numberOfRuns: $jobResults->count, 37 | countWithStatus: $this->loadCount($jobResults->batch_id) 38 | ) 39 | ); 40 | } 41 | 42 | return $batchCollection; 43 | } 44 | 45 | private function loadCount(int $batchId): array 46 | { 47 | return JobStatus::query() 48 | ->withoutEagerLoads() 49 | ->where('batch_id', $batchId) 50 | ->groupBy('status') 51 | ->select('status') 52 | ->selectRaw('COUNT(DISTINCT selector) as count') 53 | ->get() 54 | ->mapWithKeys(fn ($result) => [$result->status->value => $result->count]) 55 | ->toArray(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Search/Transformers/JobsTransformer.php: -------------------------------------------------------------------------------- 1 | groupBy('alias') 17 | ->keys(); 18 | 19 | $queryResult = JobStatus::query() 20 | ->select('alias') 21 | ->selectRaw('MAX(class) as class') 22 | ->selectRaw('MAX(created_at) as created_at') 23 | ->selectRaw('MAX(id) as id') 24 | ->selectRaw('COUNT(DISTINCT selector) as count') 25 | ->whereIn('alias', $aliases) 26 | ->groupBy('alias') 27 | ->orderBy('created_at', 'desc') 28 | ->orderBy('id', 'desc') 29 | ->get(); 30 | 31 | $trackedJobs = new TrackedJobCollection(); 32 | foreach ($queryResult as $jobResults) { 33 | // Improve this to not make any additional queries. 34 | $trackedJobs->push( 35 | new TrackedJob( 36 | $jobResults->class, 37 | $jobResults->alias, 38 | numberOfRuns: $jobResults->count, 39 | failureReasons: $this->getFailureReasons($jobResults->alias), 40 | countWithStatus: $this->loadCount($jobResults->alias) 41 | ) 42 | ); 43 | } 44 | 45 | return $trackedJobs; 46 | } 47 | 48 | private function getFailureReasons(string $alias): array 49 | { 50 | return JobException::query() 51 | ->withoutEagerLoads() 52 | ->whereHas('jobStatus', fn (Builder $query) => $query->where('alias', $alias)) 53 | ->select('message') 54 | ->selectRaw('COUNT(*) as count') 55 | ->groupBy('message') 56 | ->get() 57 | ->toArray(); 58 | } 59 | 60 | private function loadCount(string $alias): array 61 | { 62 | return JobStatus::query() 63 | ->withoutEagerLoads() 64 | ->where('alias', $alias) 65 | ->groupBy('status') 66 | ->select('status') 67 | ->selectRaw('COUNT(DISTINCT selector) as count') 68 | ->get() 69 | ->mapWithKeys(fn ($result) => [$result->status->value => $result->count]) 70 | ->toArray(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Search/Transformers/QueuesTransformer.php: -------------------------------------------------------------------------------- 1 | groupBy('queue') 15 | ->keys(); 16 | 17 | $queryResult = JobStatus::query() 18 | ->select('queue') 19 | ->selectRaw('MAX(class) as class') 20 | ->selectRaw('MAX(created_at) as created_at') 21 | ->selectRaw('MAX(id) as id') 22 | ->selectRaw('COUNT(DISTINCT selector) as count') 23 | ->whereIn('queue', $queues) 24 | ->groupBy('queue') 25 | ->orderBy('created_at', 'desc') 26 | ->orderBy('id', 'desc') 27 | ->get(); 28 | 29 | $queueCollection = new QueueCollection(); 30 | foreach ($queryResult as $jobResults) { 31 | // Improve this to not make any additional queries. 32 | $queueCollection->push( 33 | new Queue( 34 | $jobResults->queue, 35 | numberOfRuns: $jobResults->count, 36 | countWithStatus: $this->loadCount($jobResults->queue) 37 | ) 38 | ); 39 | } 40 | 41 | return $queueCollection; 42 | } 43 | 44 | private function loadCount(string $queue): array 45 | { 46 | return JobStatus::query() 47 | ->withoutEagerLoads() 48 | ->where('queue', $queue) 49 | ->groupBy('status') 50 | ->select('status') 51 | ->selectRaw('COUNT(DISTINCT selector) as count') 52 | ->get() 53 | ->mapWithKeys(fn ($result) => [$result->status->value => $result->count]) 54 | ->toArray(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Search/Transformers/RunsTransformer.php: -------------------------------------------------------------------------------- 1 | map(fn (JobStatus $jobStatus) => $jobStatus->selector); 16 | 17 | $jobs = JobStatus::whereIn('selector', $selectors) 18 | ->orderBy('created_at', 'desc') 19 | ->orderBy('id', 'desc') 20 | ->get(); 21 | 22 | $queryResult = $jobs->groupBy(['selector']); 23 | 24 | $jobRuns = new JobRunCollection(); 25 | 26 | foreach ($queryResult as $selector => $runs) { 27 | $jobRun = null; 28 | $released = new JobRunCollection(); 29 | foreach ($runs->reverse() as $run) { 30 | if ($run->status === Status::RELEASED) { 31 | $released->push(new JobRun($run)); 32 | } else { 33 | $jobRun = new JobRun($run, $jobRun, $released); 34 | $released = new JobRunCollection(); 35 | } 36 | } 37 | if ($jobRun !== null) { 38 | $jobRuns->push($jobRun); 39 | } 40 | } 41 | 42 | return $jobRuns; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Share/RetrieveConfig.php: -------------------------------------------------------------------------------- 1 | getConfig(); 15 | } 16 | 17 | public function toJson($options = 0) 18 | { 19 | return json_encode($this->toArray(), $options); 20 | } 21 | 22 | public function getConfig(): array 23 | { 24 | $baseUrl = config('laravel-job-status.routes.api.base_url'); 25 | $prefix = config('laravel-job-status.routes.api.prefix'); 26 | if ($baseUrl === null) { 27 | $baseUrl = URL::to(config('laravel-job-status.routes.api.prefix')); 28 | } else { 29 | $baseUrl = sprintf( 30 | '%s/%s', 31 | Str::endsWith($baseUrl, '/') ? Str::substr($baseUrl, 0, -1) : $baseUrl, 32 | Str::startsWith($prefix, '/') ? Str::substr($prefix, 1) : $prefix, 33 | ); 34 | } 35 | 36 | return [ 37 | 'baseUrl' => $baseUrl, 38 | ]; 39 | } 40 | 41 | public function __toString(): string 42 | { 43 | return $this->toJson(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Share/ShareConfig.php: -------------------------------------------------------------------------------- 1 | config = $config; 12 | } 13 | 14 | public function toString(): string 15 | { 16 | return $this->namespace() . $this->configAsJs(); 17 | } 18 | 19 | private function namespace(): string 20 | { 21 | return 'window.JobStatusConfig=window.JobStatusConfig||{};'; 22 | } 23 | 24 | public function configAsJs(): string 25 | { 26 | return collect($this->config->getConfig()) 27 | ->map(fn ($value, $key) => sprintf('JobStatusConfig.%s=%s;', $key, json_encode($value))) 28 | ->join(''); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/helpers.php: -------------------------------------------------------------------------------- 1 |