├── .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 |
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 | [](https://github.com/tobytwigger/laravel-job-status/releases)
22 | [](https://github.com/tobytwigger/laravel-job-status/actions/workflows/trigger_main_branch_flow.yml)
23 |
24 | [](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 | 
71 |
72 |
73 | 
74 |
75 |
76 | 
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 |