├── LICENSE.md
├── README.md
├── composer.json
├── config
├── runtime.php
└── vapor-ui.php
├── package.json
├── phpunit.xml.dist
├── public
├── app.css
├── app.js
└── mix-manifest.json
├── resources
├── css
│ ├── code-editor.css
│ ├── transitions.css
│ └── vapor-ui.css
├── js
│ ├── app.js
│ ├── base.js
│ ├── components
│ │ ├── AsyncButton.vue
│ │ ├── BarChart.vue
│ │ ├── Loader.vue
│ │ ├── Metric.vue
│ │ ├── Popover.vue
│ │ ├── Search.vue
│ │ ├── SearchDetails.vue
│ │ ├── SearchEmptyResults.vue
│ │ └── icons
│ │ │ ├── ArrowDown.vue
│ │ │ ├── ArrowUp.vue
│ │ │ ├── Calendar.vue
│ │ │ ├── ChartBar.vue
│ │ │ ├── ChevronRight.vue
│ │ │ ├── ClipboardCopy.vue
│ │ │ ├── Cloud.vue
│ │ │ ├── Collection.vue
│ │ │ ├── DesktopComputer.vue
│ │ │ ├── DocumentIcon.vue
│ │ │ ├── DotsVertical.vue
│ │ │ ├── Exclamation.vue
│ │ │ ├── Eye.vue
│ │ │ ├── Flag.vue
│ │ │ ├── InformationCircle.vue
│ │ │ ├── Loader.vue
│ │ │ ├── Refresh.vue
│ │ │ ├── Search.vue
│ │ │ ├── Sizable.js
│ │ │ ├── Terminal.vue
│ │ │ └── XCircle.vue
│ ├── mixins
│ │ ├── clipboard.js
│ │ ├── interactsWithMetrics.js
│ │ ├── interactsWithQuantity.js
│ │ ├── job.js
│ │ └── log.js
│ ├── routes.js
│ └── screens
│ │ ├── jobs
│ │ ├── index.vue
│ │ ├── metrics.vue
│ │ └── show.vue
│ │ └── logs
│ │ ├── index.vue
│ │ └── show.vue
└── views
│ └── layout.blade.php
├── routes
└── web.php
├── src
├── Concerns
│ └── ConfiguresVaporUi.php
├── Console
│ ├── InstallCommand.php
│ └── PublishCommand.php
├── Http
│ ├── Controllers
│ │ ├── HomeController.php
│ │ ├── JobController.php
│ │ ├── JobMetricController.php
│ │ └── LogController.php
│ ├── Middleware
│ │ ├── EnsureEnvironmentVariables.php
│ │ ├── EnsureUpToDateAssets.php
│ │ └── EnsureUserIsAuthorized.php
│ └── Requests
│ │ ├── JobMetricRequest.php
│ │ ├── JobRequest.php
│ │ └── LogRequest.php
├── Jobs
│ ├── ForgetFailedJob.php
│ └── RetryFailedJob.php
├── Repositories
│ ├── JobsMetricsRepository.php
│ ├── JobsRepository.php
│ └── LogsRepository.php
├── Support
│ └── Cloud.php
├── ValueObjects
│ ├── Job.php
│ ├── Log.php
│ └── SearchResult.php
└── VaporUiServiceProvider.php
├── stubs
└── VaporUiServiceProvider.stub
├── tailwind.config.js
└── webpack.mix.js
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) Taylor Otwell
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
13 | all 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
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | > This package is deprecated and will not receive Laravel `^11.0` support, as its functionality is largely replaced by Vapor's own dashboard.
2 |
3 | # Laravel Vapor UI
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | [Laravel Vapor](https://vapor.laravel.com) is an auto-scaling, serverless deployment platform for Laravel, powered by AWS Lambda. Manage your Laravel infrastructure on Vapor and fall in love with the scalability and simplicity of serverless.
19 |
20 | Vapor abstracts the complexity of managing Laravel applications on AWS Lambda, as well as interfacing those applications with SQS queues, databases, Redis clusters, networks, CloudFront CDN, and more.
21 |
22 | This package provides a beautiful dashboard accessible via your Vapor application that allows you to view / search your application's logs and failed queue jobs.
23 |
24 | 
25 |
26 | ## Official Documentation
27 |
28 | Documentation for Vapor UI can be found in the [Laravel Vapor documentation](https://docs.vapor.build/1.0/introduction.html#installing-the-vapor-ui-dashboard).
29 |
30 | ## Contributing
31 |
32 | Thank you for considering contributing to Vapor UI! You can read the contribution guide [here](.github/CONTRIBUTING.md).
33 |
34 | ## Code of Conduct
35 |
36 | In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
37 |
38 | ## Security Vulnerabilities
39 |
40 | Please review [our security policy](https://github.com/laravel/vapor-ui/security/policy) on how to report security vulnerabilities.
41 |
42 | ## License
43 |
44 | Laravel Vapor UI is open-sourced software licensed under the [MIT license](LICENSE.md).
45 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "laravel/vapor-ui",
3 | "description": "The Laravel Vapor UI.",
4 | "keywords": ["laravel", "vapor"],
5 | "license": "MIT",
6 | "support": {
7 | "issues": "https://github.com/laravel/vapor-ui/issues",
8 | "source": "https://github.com/laravel/vapor-ui"
9 | },
10 | "authors": [
11 | {
12 | "name": "Taylor Otwell",
13 | "email": "taylor@laravel.com"
14 | },
15 | {
16 | "name": "Nuno Maduro",
17 | "email": "nuno@laravel.com"
18 | }
19 | ],
20 | "require": {
21 | "php": "^7.3|^8.0",
22 | "aws/aws-sdk-php": "^3.148.3",
23 | "laravel/framework": "^8.0|^9.0|^10.0",
24 | "symfony/console": "^5.3|^6.0",
25 | "symfony/yaml": "^5.1.4|^6.0"
26 | },
27 | "require-dev": {
28 | "orchestra/testbench": "^6.17.1|^7.0|^8.0",
29 | "pestphp/pest": "^1.22.3"
30 | },
31 | "autoload": {
32 | "psr-4": {
33 | "Laravel\\VaporUi\\": "src/"
34 | }
35 | },
36 | "autoload-dev": {
37 | "psr-4": {
38 | "Tests\\": "tests/"
39 | }
40 | },
41 | "extra": {
42 | "branch-alias": {
43 | "dev-master": "1.x-dev"
44 | },
45 | "laravel": {
46 | "providers": [
47 | "Laravel\\VaporUi\\VaporUiServiceProvider"
48 | ]
49 | }
50 | },
51 | "config": {
52 | "sort-packages": true,
53 | "allow-plugins": {
54 | "pestphp/pest-plugin": true
55 | }
56 | },
57 | "minimum-stability": "dev",
58 | "prefer-stable": true
59 | }
60 |
--------------------------------------------------------------------------------
/config/runtime.php:
--------------------------------------------------------------------------------
1 | env('AWS_ACCESS_KEY_ID'),
8 | 'secret' => env('AWS_SECRET_ACCESS_KEY'),
9 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
10 |
11 | // Vapor Project / CloudWatch Log Group
12 | 'project' => env('VAPOR_PROJECT', Cloud::project()),
13 | 'environment' => env('VAPOR_ENVIRONMENT', Cloud::environment()),
14 |
15 | // SQS Queue
16 | 'queue' => [
17 | 'prefix' => env('SQS_PREFIX'),
18 | 'name' => env('SQS_QUEUE'),
19 | ],
20 | ];
21 |
--------------------------------------------------------------------------------
/config/vapor-ui.php:
--------------------------------------------------------------------------------
1 | [
21 | 'web',
22 | EnsureUserIsAuthorized::class,
23 | EnsureEnvironmentVariables::class,
24 | EnsureUpToDateAssets::class,
25 | ],
26 |
27 | /*
28 | |--------------------------------------------------------------------------
29 | | Vapor UI Domain
30 | |--------------------------------------------------------------------------
31 | |
32 | | This is the subdomain where Vapor UI will be accessible from. If this
33 | | setting is null, Vapor UI will reside under the same domain as the
34 | | application. Otherwise this value should serve as the subdomain.
35 | |
36 | */
37 |
38 | 'domain' => env('VAPOR_UI_DOMAIN', null),
39 |
40 | /*
41 | |--------------------------------------------------------------------------
42 | | Vapor UI Path
43 | |--------------------------------------------------------------------------
44 | |
45 | | This is the URI path where Vapor UI will be accessible from. Feel free
46 | | to change the path to anything you like. Note that the URI will not
47 | | affect the paths of its internal API that isn't exposed to users.
48 | |
49 | */
50 |
51 | 'path' => env('VAPOR_UI_PATH', 'vapor-ui'),
52 |
53 | /*
54 | |--------------------------------------------------------------------------
55 | | Vapor UI Queues
56 | |--------------------------------------------------------------------------
57 | |
58 | | Typically, queues that should be monitored will be determined for you
59 | | by Vapor UI. However, you are free to add additional queues to the
60 | | list below in order to monitor queues that Vapor doesn't manage.
61 | |
62 | */
63 |
64 | 'queues' => [
65 |
66 | ],
67 |
68 | ];
69 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "dev": "npm run development",
5 | "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
6 | "watch": "npm run development -- --watch",
7 | "watch-poll": "npm run watch -- --watch-poll",
8 | "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
9 | "prod": "npm run production",
10 | "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
11 | },
12 | "devDependencies": {
13 | "@tailwindcss/ui": "^0.5.0",
14 | "axios": "^0.19",
15 | "chart.js": "^2.9.3",
16 | "cross-env": "^7.0",
17 | "laravel-mix": "^5.0.1",
18 | "moment": "^2.27.0",
19 | "moment-timezone": "^0.5.31",
20 | "postcss-import": "^12.0.1",
21 | "tailwindcss": "^1.6.2",
22 | "vue": "^2.6.11",
23 | "vue-json-pretty": "^1.6.7",
24 | "vue-popperjs": "^2.3.0",
25 | "vue-router": "^3.0.1",
26 | "vue-template-compiler": "^2.5.21"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
16 | ./tests/Unit
17 |
18 |
19 | ./tests/Integration
20 |
21 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/public/mix-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "/app.js": "/app.js?id=44c148984c42d320dccb",
3 | "/app.css": "/app.css?id=fbb793d795cfb9fe09b2"
4 | }
5 |
--------------------------------------------------------------------------------
/resources/css/code-editor.css:
--------------------------------------------------------------------------------
1 | .vjs-tree__content {
2 | border-left: 1px dotted rgba(204, 204, 204, 0.28) !important;
3 | }
4 | .vjs-tree__node {
5 | cursor: pointer;
6 | &:hover {
7 | color: #20a0ff;
8 | }
9 | }
10 | .vjs-checkbox {
11 | position: absolute;
12 | left: -30px;
13 | }
14 | .vjs-value__null {
15 | color: #a291f5 !important;
16 | }
17 | .vjs-value__number,
18 | .vjs-value__boolean {
19 | color: #a291f5 !important;
20 | }
21 | .vjs-value__string {
22 | color: #dacb4d !important;
23 | }
24 |
25 | .hljs-keyword,
26 | .hljs-selector-tag,
27 | .hljs-addition {
28 | color: #8bd72f;
29 | }
30 |
31 | .hljs-string,
32 | .hljs-meta .hljs-meta-string,
33 | .hljs-doctag,
34 | .hljs-regexp {
35 | color: #dacb4d;
36 | }
37 |
38 | .hljs-number,
39 | .hljs-literal {
40 | color: #a291f5 !important;
41 | }
42 |
--------------------------------------------------------------------------------
/resources/css/transitions.css:
--------------------------------------------------------------------------------
1 | .fade-enter-active,
2 | .fade-leave-active {
3 | transition: opacity 0.5s;
4 | }
5 | .fade-enter,
6 | .fade-leave-to {
7 | opacity: 0;
8 | }
9 |
10 | .list-enter-active,
11 | .list-leave-active,
12 | .list-move {
13 | transition: 50ms cubic-bezier(0.59, 0.12, 0.34, 0.95);
14 | transition-property: opacity, transform;
15 | }
16 |
17 | .list-enter {
18 | opacity: 0;
19 | transform: translateX(50px) scaleY(0.5);
20 | }
21 |
22 | .list-enter-to {
23 | opacity: 1;
24 | transform: translateX(0) scaleY(1);
25 | }
26 |
27 | .list-leave-active {
28 | position: absolute;
29 | }
30 |
31 | .list-leave-to {
32 | opacity: 0;
33 | transform: scaleY(0);
34 | transform-origin: center top;
35 | }
36 |
--------------------------------------------------------------------------------
/resources/css/vapor-ui.css:
--------------------------------------------------------------------------------
1 | @import 'tailwindcss/base';
2 |
3 | @import 'tailwindcss/components';
4 |
5 | @import 'tailwindcss/utilities';
6 |
7 | @import './code-editor.css';
8 |
9 | @import './transitions.css';
10 |
--------------------------------------------------------------------------------
/resources/js/app.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import Base from './base';
3 | import moment from 'moment-timezone';
4 | import Routes from './routes';
5 | import Vue from 'vue';
6 | import VueJsonPretty from 'vue-json-pretty';
7 | import VueRouter from 'vue-router';
8 |
9 | const token = document.head.querySelector('meta[name="csrf-token"]');
10 |
11 | if (token) {
12 | axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content;
13 | }
14 |
15 | Vue.use(VueRouter);
16 |
17 | moment.tz.setDefault('utc');
18 |
19 | const router = new VueRouter({
20 | routes: Routes,
21 | mode: 'history',
22 | base: '/' + window.VaporUi.path,
23 | });
24 |
25 | router.beforeEach((to, from, next) => {
26 | to.meta.title = to.meta.createTitle(to.params);
27 |
28 | document.title = 'Vapor UI - ' + to.meta.title;
29 |
30 | next();
31 | });
32 |
33 | Vue.component('vue-json-pretty', VueJsonPretty);
34 |
35 | // Components
36 | Vue.component('async-button', require('./components/AsyncButton.vue').default);
37 | Vue.component('bar-chart', require('./components/BarChart.vue').default);
38 | Vue.component('search', require('./components/Search.vue').default);
39 | Vue.component('search-details', require('./components/SearchDetails.vue').default);
40 | Vue.component('search-empty-results', require('./components/SearchEmptyResults.vue').default);
41 | Vue.component('loader', require('./components/Loader.vue').default);
42 | Vue.component('metric', require('./components/Metric.vue').default);
43 | Vue.component('popover', require('./components/Popover.vue').default);
44 |
45 | // Icons
46 | Vue.component('icon-arrow-down', require('./components/icons/ArrowDown.vue').default);
47 | Vue.component('icon-arrow-up', require('./components/icons/ArrowUp.vue').default);
48 | Vue.component('icon-refresh', require('./components/icons/Refresh.vue').default);
49 | Vue.component('icon-search', require('./components/icons/Search.vue').default);
50 | Vue.component('icon-cloud', require('./components/icons/Cloud.vue').default);
51 | Vue.component('icon-collection', require('./components/icons/Collection.vue').default);
52 | Vue.component('icon-exclamation', require('./components/icons/Exclamation.vue').default);
53 | Vue.component('icon-desktop-computer', require('./components/icons/DesktopComputer.vue').default);
54 | Vue.component('icon-dots-vertical', require('./components/icons/DotsVertical.vue').default);
55 | Vue.component('icon-loader', require('./components/icons/Loader.vue').default);
56 | Vue.component('icon-flag', require('./components/icons/Flag.vue').default);
57 | Vue.component('icon-calendar', require('./components/icons/Calendar.vue').default);
58 | Vue.component('icon-clipboard-copy', require('./components/icons/ClipboardCopy.vue').default);
59 | Vue.component('icon-chevron-right', require('./components/icons/ChevronRight.vue').default);
60 | Vue.component('icon-eye', require('./components/icons/Eye.vue').default);
61 | Vue.component('icon-chart-bar', require('./components/icons/ChartBar.vue').default);
62 | Vue.component('icon-terminal', require('./components/icons/Terminal.vue').default);
63 | Vue.component('icon-x-circle', require('./components/icons/XCircle.vue').default);
64 | Vue.component('icon-information-circle', require('./components/icons/InformationCircle.vue').default);
65 |
66 | Vue.mixin(Base);
67 |
68 | new Vue({
69 | el: '#vapor-ui',
70 | router,
71 | });
72 |
--------------------------------------------------------------------------------
/resources/js/base.js:
--------------------------------------------------------------------------------
1 | import _ from 'lodash';
2 | import moment from 'moment-timezone';
3 |
4 | export default {
5 | methods: {
6 | /**
7 | * Returns an moment instance.
8 | */
9 | moment() {
10 | return moment;
11 | },
12 |
13 | /**
14 | * Creates a debounced function that delays invoking a callback.
15 | */
16 | debouncer: _.debounce((callback) => callback(), 500),
17 | },
18 | };
19 |
--------------------------------------------------------------------------------
/resources/js/components/AsyncButton.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
30 |
--------------------------------------------------------------------------------
/resources/js/components/BarChart.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
107 |
--------------------------------------------------------------------------------
/resources/js/components/Loader.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
41 |
--------------------------------------------------------------------------------
/resources/js/components/Metric.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{ title }}
9 |
10 |
11 | {{ description }}
12 |
13 |
14 |
15 |
16 | {{ title }}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | {{ formatQuantity(value) }}
25 |
26 |
27 |
28 | Usually {{ formatQuantity(average) }} / hr
29 |
30 |
31 |
32 | {{ 'Average based in the last 24 hours prior to the current hour.' }}
33 |
34 |
35 |
36 |
37 |
46 |
52 |
58 | Increased/Decreased by
59 | {{ formatQuantity(percentage()) }}%
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
130 |
--------------------------------------------------------------------------------
/resources/js/components/Popover.vue:
--------------------------------------------------------------------------------
1 |
2 |
9 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
45 |
--------------------------------------------------------------------------------
/resources/js/components/Search.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
23 |
24 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
Starting from
44 |
45 |
46 |
47 |
48 |
54 |
55 |
61 |
66 | {{ label }}
67 |
68 |
69 |
70 |
71 |
72 | {{ error }}
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | No entries have been found yet, still searching...
88 |
89 |
90 |
91 |
92 |
93 | No entries have been found close to the given "Starting from" date.
94 |
95 | Please adjust the "Starting from" date, or
96 | click here to keep searching .
102 |
103 |
104 | No entries were found for the given search criteria.
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 | Server Error
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
129 |
130 |
133 | {{ this.title }}
134 |
135 |
136 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
388 |
--------------------------------------------------------------------------------
/resources/js/components/SearchDetails.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
16 | {{
17 | $router
18 | .resolve({ name: `${$route.meta.resource}-index` })
19 | .route.meta.createTitle({ group: $route.params.group })
20 | }}
21 |
22 |
23 | Details
26 |
27 |
28 |
29 |
30 |
31 | Copied to clipboard
32 |
33 |
34 |
39 | Share
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
Entry not found.
53 |
54 |
55 |
56 |
57 |
58 | {{ $route.meta.title }}
59 |
60 |
61 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
135 |
--------------------------------------------------------------------------------
/resources/js/components/SearchEmptyResults.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
22 |
--------------------------------------------------------------------------------
/resources/js/components/icons/ArrowDown.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
14 |
--------------------------------------------------------------------------------
/resources/js/components/icons/ArrowUp.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
18 |
--------------------------------------------------------------------------------
/resources/js/components/icons/Calendar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
18 |
--------------------------------------------------------------------------------
/resources/js/components/icons/ChartBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
19 |
--------------------------------------------------------------------------------
/resources/js/components/icons/ChevronRight.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
18 |
--------------------------------------------------------------------------------
/resources/js/components/icons/ClipboardCopy.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
17 |
--------------------------------------------------------------------------------
/resources/js/components/icons/Cloud.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
17 |
--------------------------------------------------------------------------------
/resources/js/components/icons/Collection.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
16 |
--------------------------------------------------------------------------------
/resources/js/components/icons/DesktopComputer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
18 |
--------------------------------------------------------------------------------
/resources/js/components/icons/DocumentIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
7 |
8 |
9 |
10 |
11 |
18 |
--------------------------------------------------------------------------------
/resources/js/components/icons/DotsVertical.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
14 |
--------------------------------------------------------------------------------
/resources/js/components/icons/Exclamation.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
19 |
--------------------------------------------------------------------------------
/resources/js/components/icons/Eye.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 |
12 |
19 |
--------------------------------------------------------------------------------
/resources/js/components/icons/Flag.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
18 |
--------------------------------------------------------------------------------
/resources/js/components/icons/InformationCircle.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
19 |
--------------------------------------------------------------------------------
/resources/js/components/icons/Loader.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
14 |
24 |
25 |
26 |
36 |
46 |
47 |
48 |
58 |
68 |
69 |
70 |
71 |
72 |
79 |
--------------------------------------------------------------------------------
/resources/js/components/icons/Refresh.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
18 |
--------------------------------------------------------------------------------
/resources/js/components/icons/Search.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
18 |
--------------------------------------------------------------------------------
/resources/js/components/icons/Sizable.js:
--------------------------------------------------------------------------------
1 | export default {
2 | props: {
3 | size: {
4 | default: '8',
5 | },
6 | },
7 |
8 | computed: {
9 | sizeClass() {
10 | return {
11 | 2: 'h-2 w-2',
12 | 3: 'h-3 w-3',
13 | 4: 'h-4 w-4',
14 | 5: 'h-5 w-5',
15 | 6: 'h-6 w-6',
16 | 8: 'h-8 w-8',
17 | 12: 'h-12 w-12',
18 | 16: 'h-16 w-16',
19 | 20: 'h-20 w-20',
20 | }[this.size];
21 | },
22 | },
23 | };
24 |
--------------------------------------------------------------------------------
/resources/js/components/icons/Terminal.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
18 |
--------------------------------------------------------------------------------
/resources/js/components/icons/XCircle.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
8 |
9 |
10 |
11 |
18 |
--------------------------------------------------------------------------------
/resources/js/mixins/clipboard.js:
--------------------------------------------------------------------------------
1 | export default {
2 | methods: {
3 | copyToClipboard(href) {
4 | const el = document.createElement('textarea');
5 | const base = window.location;
6 | el.value = base.protocol + '//' + base.host + href;
7 | el.setAttribute('readonly', '');
8 | el.style.position = 'absolute';
9 | el.style.left = '-9999px';
10 | document.body.appendChild(el);
11 | el.select();
12 | document.execCommand('copy');
13 | document.body.removeChild(el);
14 | },
15 | },
16 | };
17 |
--------------------------------------------------------------------------------
/resources/js/mixins/interactsWithMetrics.js:
--------------------------------------------------------------------------------
1 | import moment from 'moment';
2 |
3 | import InteractsWithQuantiy from './interactsWithQuantity';
4 |
5 | export default {
6 | /**
7 | * The mixin's mixins.
8 | */
9 | mixins: [InteractsWithQuantiy],
10 |
11 | /**
12 | * The mixin's methods.
13 | */
14 | methods: {
15 | /**
16 | * Formats the give date to a human readable format.
17 | */
18 | formatMetricDate(date) {
19 | return moment
20 | .utc(date * 1000, 'x')
21 | .local()
22 | .format('LT')
23 | .replace(/:[0-9]{2}/, '');
24 | },
25 |
26 | /**
27 | * Formats the given label to a tooltip title.
28 | */
29 | formatTooltipTitle([{ label }]) {
30 | return (
31 | label +
32 | ' - ' +
33 | moment(label, 'LT')
34 | .add(1, 'hours')
35 | .local(true)
36 | .format('LT')
37 | .replace(/:[0-9]{2}/, '')
38 | );
39 | },
40 |
41 | /**
42 | * Gets the max value of the given timeseries.
43 | */
44 | suggestedMax(timeseries) {
45 | const data = timeseries.map((point) => point.value);
46 |
47 | return Math.max(Math.max(...data), 1);
48 | },
49 | },
50 | };
51 |
--------------------------------------------------------------------------------
/resources/js/mixins/interactsWithQuantity.js:
--------------------------------------------------------------------------------
1 | import moment from 'moment';
2 |
3 | export default {
4 | /**
5 | * The mixin's methods.
6 | */
7 | methods: {
8 | /**
9 | * Formats the given quantity to a human readable format.
10 | */
11 | formatQuantity(quantity) {
12 | const replace = (num, division, unit) => {
13 | return (num / division).toFixed(1).replace(/\.0$/, '') + unit;
14 | };
15 |
16 | if (quantity >= 1000000) {
17 | return replace(quantity, 1000000, 'M');
18 | } else if (quantity >= 1000) {
19 | return replace(quantity, 1000, 'K');
20 | } else if (quantity <= -1000000) {
21 | return replace(quantity, -1000000, 'M');
22 | } else if (quantity <= -1000) {
23 | return replace(quantity, -1000, 'K');
24 | }
25 |
26 | return quantity;
27 | },
28 | },
29 | };
30 |
--------------------------------------------------------------------------------
/resources/js/mixins/job.js:
--------------------------------------------------------------------------------
1 | export default {
2 | methods: {
3 | /**
4 | * Returns the log color by the given display name.
5 | */
6 | jobColor(displayName) {
7 | return 'red';
8 | },
9 |
10 | /**
11 | * Returns the queue names.
12 | */
13 | queues() {
14 | let queues = {};
15 |
16 | VaporUi.queues.forEach((queue) => {
17 | if (typeof queue == 'object') {
18 | let q = Object.keys(queue)[0] || null;
19 |
20 | if (q) {
21 | queues[q] = q;
22 | return;
23 | }
24 | }
25 |
26 | if (typeof queue == 'string') {
27 | queues[queue] = queue;
28 | return;
29 | }
30 | });
31 |
32 | return queues;
33 | },
34 | },
35 | };
36 |
--------------------------------------------------------------------------------
/resources/js/mixins/log.js:
--------------------------------------------------------------------------------
1 | export default {
2 | methods: {
3 | /**
4 | * Returns the log types.
5 | */
6 | logTypes() {
7 | return {
8 | debug: 'Debug',
9 | info: 'Info',
10 | notice: 'Notice',
11 | warning: 'Warning',
12 | alert: 'Alert',
13 | critical: 'Critical',
14 | emergency: 'Emergency',
15 | timeout: 'Timeout',
16 | error: 'Error',
17 | };
18 | },
19 |
20 | /**
21 | * Returns the log color by the given type.
22 | */
23 | logColor(type) {
24 | const compare = (level) => level === type.toLowerCase();
25 |
26 | if (['info', 'notice'].some(compare)) {
27 | return 'blue';
28 | }
29 |
30 | if (['warning', 'alert'].some(compare)) {
31 | return 'yellow';
32 | }
33 |
34 | if (['critical', 'emergency', 'error', 'timeout'].some(compare)) {
35 | return 'red';
36 | }
37 |
38 | return 'gray';
39 | },
40 | },
41 | };
42 |
--------------------------------------------------------------------------------
/resources/js/routes.js:
--------------------------------------------------------------------------------
1 | export default [
2 | { path: '/', redirect: '/logs/http' },
3 |
4 | {
5 | path: '/logs/:group',
6 | name: 'logs-index',
7 | component: require('./screens/logs/index').default,
8 | meta: {
9 | resource: 'logs',
10 | createTitle: ({ group }) => group.toUpperCase() + ' Logs',
11 | },
12 | },
13 |
14 | {
15 | path: '/logs/:group/:id',
16 | name: 'logs-show',
17 | component: require('./screens/logs/show').default,
18 | meta: {
19 | resource: 'logs',
20 | createTitle: ({ group }) => group.toUpperCase() + ' Logs - Details',
21 | },
22 | },
23 |
24 | {
25 | path: '/jobs/metrics',
26 | name: 'jobs-metrics',
27 | component: require('./screens/jobs/metrics').default,
28 | meta: {
29 | resource: 'jobs',
30 | createTitle: () => 'Jobs Metrics',
31 | },
32 | },
33 |
34 | {
35 | path: '/jobs/:group',
36 | name: 'jobs-index',
37 | component: require('./screens/jobs/index').default,
38 | meta: {
39 | resource: 'jobs',
40 | createTitle: ({ group }) => group.toUpperCase() + ' Jobs',
41 | },
42 | },
43 |
44 | {
45 | path: '/jobs/:group/:id',
46 | name: 'jobs-show',
47 | component: require('./screens/jobs/show').default,
48 | meta: {
49 | resource: 'jobs',
50 | createTitle: ({ group }) => group.toUpperCase() + ' Jobs - Details',
51 | },
52 | },
53 | ];
54 |
--------------------------------------------------------------------------------
/resources/js/screens/jobs/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Queue name
8 |
9 |
15 | {{ label }}
16 |
17 |
18 |
19 |
20 |
21 | It looks like there was an error. Please check your application logs.
22 |
23 |
24 | This error may occur if you haven't properly configured "failed jobs" in your application.
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 | {{ entry.message }}
34 |
35 |
36 |
37 | {{ entry.exception }}
38 |
39 |
40 |
41 |
42 |
43 |
48 | {{ entry.content.payload.displayName }}
49 |
50 |
51 |
52 |
53 | {{ moment().utc(entry.timestamp, 'x').local().format('YYYY-MM-DD LTS') }}
54 |
55 |
56 |
57 |
67 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |
88 |
--------------------------------------------------------------------------------
/resources/js/screens/jobs/metrics.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | Queue name
12 |
13 |
19 | {{ label }}
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
Server Error
36 |
37 |
38 |
39 |
It looks like there was an error. Please check your application logs.
40 |
41 |
42 |
43 |
44 |
45 |
46 |
Overview
47 |
48 |
49 |
50 |
56 |
57 |
64 |
65 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
Processed Jobs
77 |
78 |
79 |
80 |
81 |
82 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
Failed Jobs
104 |
105 |
106 |
107 |
108 |
109 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
203 |
--------------------------------------------------------------------------------
/resources/js/screens/jobs/show.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
12 | Retry
13 |
14 |
15 |
16 |
23 | Forget
24 |
25 |
26 |
27 |
28 |
29 |
30 |
Time ({{ moment().tz.guess() }})
31 |
32 |
33 | {{ moment().utc(entry.timestamp, 'x').local().format('YYYY-MM-DD LTS') }}
34 |
35 |
36 | {{ moment().utc(entry.timestamp, 'x').fromNow() }}
37 |
38 |
39 |
40 |
41 |
42 |
ID
43 |
44 | {{ entry.id }}
45 |
46 |
47 |
48 |
52 |
UUID
53 |
54 | {{ entry.content.uuid }}
55 |
56 |
57 |
58 |
59 |
Connection
60 |
61 | {{ entry.content.connection }}
62 |
63 |
64 |
65 |
Queue
66 |
67 | {{ entry.content.queue }}
68 |
69 |
70 |
71 |
72 |
Name
73 |
74 | {{ entry.content.payload.displayName }}
75 |
76 |
77 |
78 |
79 |
Message
80 |
81 | {{ entry.message }}
82 |
83 |
84 |
85 |
109 |
110 |
111 |
115 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 | {{ line }}
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
190 |
--------------------------------------------------------------------------------
/resources/js/screens/logs/index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Log type
7 |
13 | All
14 | {{ label }}
15 |
16 |
17 |
18 |
19 |
20 |
21 | It looks like there was an error. Please check your application logs.
22 |
23 |
24 | Consider searching using a more recent "Starting from" date. The CloudWatch API may have long response
25 | times while searching far into the past. These requests may timeout or lead to unexpected errors.
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | {{ entry.content.message.message }}
36 |
37 |
38 | {{ entry.content.message.output }}
39 |
40 |
41 | {{ entry.content.message }}
42 |
43 |
44 |
45 |
53 | {{ entry.content.message.context.exception.class }}
54 |
55 |
56 |
57 |
58 |
59 |
64 | {{ entry.type }}
65 |
66 |
67 |
68 | {{ moment().utc(entry.content.timestamp, 'x').local().format('YYYY-MM-DD LTS') }}
69 |
70 |
71 |
72 |
80 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
101 |
--------------------------------------------------------------------------------
/resources/js/screens/logs/show.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Time ({{ moment().tz.guess() }})
6 |
7 |
8 | {{ moment().utc(entry.content.timestamp, 'x').local().format('YYYY-MM-DD LTS') }}
9 |
10 |
11 | {{ moment().utc(entry.content.timestamp, 'x').fromNow() }}
12 |
13 |
14 |
15 |
16 |
17 |
ID
18 |
19 | {{ entry.id }}
20 |
21 |
22 |
23 |
24 |
Log Stream Name
25 |
26 | {{ entry.content.logStreamName }}
27 |
28 |
29 |
33 |
Request ID
34 |
35 | {{ entry.requestId }}
36 |
37 |
38 |
39 |
40 |
Type
41 |
42 | {{ entry.type }}
43 |
44 |
45 |
46 |
50 |
Location
51 |
52 | {{ entry.location }}
53 |
54 |
55 |
56 |
60 |
Command
61 |
62 | {{ entry.content.message.context.command }}
63 |
64 |
65 |
66 |
70 |
Message
71 |
72 | {{ entry.content.message.message ? entry.content.message.message : entry.content.message }}
73 |
74 |
75 |
79 |
80 |
81 |
82 | {{ entry.content.message.output.trim() }}
83 |
84 |
85 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
109 |
--------------------------------------------------------------------------------
/resources/views/layout.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | Vapor UI - {{ config('vapor-ui.project') }} - {{ config('vapor-ui.environment') }}
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
Laravel Vapor
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | {{ config('vapor-ui.project') }} > {{ config('vapor-ui.environment') }}
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | {{ config('vapor-ui.region') }}
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | Logs
52 |
53 |
54 |
55 |
61 |
62 | HTTP
63 |
64 |
70 |
71 | CLI
72 |
73 |
79 |
80 | Queue
81 |
82 |
83 |
84 |
85 |
86 | Jobs
87 |
88 |
89 |
90 |
96 |
97 | Metrics
98 |
99 |
100 |
106 |
107 | Failed
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
125 |
126 |
127 |
128 |
129 |
--------------------------------------------------------------------------------
/routes/web.php:
--------------------------------------------------------------------------------
1 | group(function () {
11 | Route::get('/api/logs/{group}', [LogController::class, 'index']);
12 | Route::get('/api/logs/{group}/{id}', [LogController::class, 'show']);
13 |
14 | Route::get('/api/jobs/metrics', [JobMetricController::class, 'index']);
15 |
16 | Route::get('/api/jobs/{group}', [JobController::class, 'index']);
17 | Route::get('/api/jobs/{group}/{id}', [JobController::class, 'show']);
18 |
19 | Route::post('/api/jobs/failed/retry/{id}', [JobController::class, 'retry']);
20 | Route::post('/api/jobs/failed/forget/{id}', [JobController::class, 'forget']);
21 |
22 | Route::get('/{view?}', HomeController::class)->where('view', '(.*)')->name('vapor-ui');
23 | });
24 |
--------------------------------------------------------------------------------
/src/Concerns/ConfiguresVaporUi.php:
--------------------------------------------------------------------------------
1 | $_ENV['AWS_ACCESS_KEY_ID'] ?? null,
23 | 'secret' => $_ENV['AWS_SECRET_ACCESS_KEY'] ?? null,
24 | 'region' => $_ENV['AWS_DEFAULT_REGION'] ?? 'us-east-1',
25 | 'token' => $_ENV['AWS_SESSION_TOKEN'] ?? null,
26 | 'queue' => [
27 | 'prefix' => $_ENV['SQS_PREFIX'] ?? null,
28 | 'name' => $_ENV['SQS_QUEUE'] ?? null,
29 | ],
30 | ], Config::get('vapor-ui') ?? []));
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/Console/InstallCommand.php:
--------------------------------------------------------------------------------
1 | comment('Publishing Vapor UI Service Provider...');
34 | $this->callSilent('vendor:publish', ['--tag' => 'vapor-ui-provider']);
35 |
36 | $this->comment('Publishing Vapor UI Assets...');
37 | $this->callSilent('vendor:publish', ['--tag' => 'vapor-ui-assets']);
38 |
39 | $this->registerVaporUiServiceProvider();
40 |
41 | $this->info('Vapor UI scaffolding installed successfully.');
42 | }
43 |
44 | /**
45 | * Register the Vapor UI service provider in the application configuration file.
46 | *
47 | * @return void
48 | */
49 | protected function registerVaporUiServiceProvider()
50 | {
51 | $namespace = Str::replaceLast('\\', '', $this->laravel->getNamespace());
52 |
53 | $appConfig = file_get_contents(config_path('app.php'));
54 |
55 | if (Str::contains($appConfig, $namespace.'\\Providers\\VaporUiServiceProvider::class')) {
56 | return;
57 | }
58 |
59 | $lineEndingCount = [
60 | "\r\n" => substr_count($appConfig, "\r\n"),
61 | "\r" => substr_count($appConfig, "\r"),
62 | "\n" => substr_count($appConfig, "\n"),
63 | ];
64 |
65 | $eol = array_keys($lineEndingCount, max($lineEndingCount))[0];
66 |
67 | file_put_contents(config_path('app.php'), str_replace(
68 | "{$namespace}\\Providers\RouteServiceProvider::class,".$eol,
69 | "{$namespace}\\Providers\RouteServiceProvider::class,".$eol." {$namespace}\Providers\VaporUiServiceProvider::class,".$eol,
70 | $appConfig
71 | ));
72 |
73 | file_put_contents(app_path('Providers/VaporUiServiceProvider.php'), str_replace(
74 | "namespace App\Providers;",
75 | "namespace {$namespace}\Providers;",
76 | file_get_contents(app_path('Providers/VaporUiServiceProvider.php'))
77 | ));
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/Console/PublishCommand.php:
--------------------------------------------------------------------------------
1 | call('vendor:publish', [
33 | '--tag' => 'vapor-ui-assets',
34 | '--force' => true,
35 | ]);
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/Http/Controllers/HomeController.php:
--------------------------------------------------------------------------------
1 | config('vapor-ui.path', 'vapor-ui'),
19 | 'queues' => Cloud::queues(),
20 | ]);
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/Http/Controllers/JobController.php:
--------------------------------------------------------------------------------
1 | repository = $repository;
29 | }
30 |
31 | /**
32 | * Gets the job results by the given request filters.
33 | *
34 | * @param string $group
35 | * @return SearchResult
36 | */
37 | public function index($group, JobRequest $request)
38 | {
39 | return $this->repository->search($group, $request->validated());
40 | }
41 |
42 | /**
43 | * Gets a job by the given request filters.
44 | *
45 | * @param string $group
46 | * @param string $id
47 | * @return Job|null
48 | */
49 | public function show($group, $id, JobRequest $request)
50 | {
51 | return $this->repository->get($group, $id, $request->validated());
52 | }
53 |
54 | /**
55 | * Retry a job by the given id.
56 | *
57 | * @param string $id
58 | * @return void
59 | */
60 | public function retry($id)
61 | {
62 | dispatch_sync(new RetryFailedJob($id));
63 | }
64 |
65 | /**
66 | * Forget a job by the given id.
67 | *
68 | * @param string $id
69 | * @return void
70 | */
71 | public function forget($id)
72 | {
73 | dispatch_sync(new ForgetFailedJob($id));
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/Http/Controllers/JobMetricController.php:
--------------------------------------------------------------------------------
1 | metrics = $metrics;
26 | }
27 |
28 | /**
29 | * Gets multiple metrics about jobs.
30 | *
31 | * @return array
32 | */
33 | public function index(JobMetricRequest $request)
34 | {
35 | tap($request->get('queue'), function ($queue) {
36 | if ($queue) {
37 | $this->metrics->resolveQueueUsing(function () use ($queue) {
38 | return $queue;
39 | });
40 | }
41 | });
42 |
43 | return [
44 | 'failed' => [
45 | 'timeseries' => $this->metrics->failedTimeseries(),
46 | 'pastHour' => $this->metrics->failedSumByHoursAgo(1),
47 | 'averagePast24Hours' => $this->metrics->failedAverageByHoursAgo(25, 1),
48 | ],
49 | 'processed' => [
50 | 'timeseries' => $this->metrics->processedTimeseries(),
51 | 'pastHour' => $this->metrics->processedSumByHoursAgo(1),
52 | 'averagePast24Hours' => $this->metrics->processedAverageByHoursAgo(25, 1),
53 | ],
54 | 'pending' => $this->metrics->pending(),
55 | ];
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Http/Controllers/LogController.php:
--------------------------------------------------------------------------------
1 | repository = $repository;
28 | }
29 |
30 | /**
31 | * Gets the log results by the given request filters.
32 | *
33 | * @param string $group
34 | * @param LogRequest $request
35 | * @return SearchResult
36 | */
37 | public function index($group, LogRequest $request)
38 | {
39 | return $this->repository->search($group, $request->validated());
40 | }
41 |
42 | /**
43 | * Gets a log by the given request filters.
44 | *
45 | * @param string $group
46 | * @param string $id
47 | * @param LogRequest $request
48 | * @return Log|null
49 | */
50 | public function show($group, $id, LogRequest $request)
51 | {
52 | return $this->repository->get($group, $id, $request->validated());
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/Http/Middleware/EnsureEnvironmentVariables.php:
--------------------------------------------------------------------------------
1 | configs)->each(function ($name) use ($message, $config) {
39 | if (empty(Arr::get($config, $name))) {
40 | throw new RuntimeException(sprintf($message, $name));
41 | }
42 | });
43 |
44 | return $next($request);
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/Http/Middleware/EnsureUpToDateAssets.php:
--------------------------------------------------------------------------------
1 | runningInConsole()) {
23 | $publishedPath = public_path('vendor/vapor-ui/mix-manifest.json');
24 |
25 | if (! File::exists($publishedPath)) {
26 | throw new RuntimeException('The Vapor UI assets are not published. Please run: `php artisan vapor-ui:install.');
27 | }
28 |
29 | if (File::get($publishedPath) !== File::get(__DIR__.'/../../../public/mix-manifest.json')) {
30 | throw new RuntimeException('The published Vapor UI assets are not up-to-date with the installed version. Please run: `php artisan vapor-ui:publish`.');
31 | }
32 | }
33 |
34 | return $next($request);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/Http/Middleware/EnsureUserIsAuthorized.php:
--------------------------------------------------------------------------------
1 | environment('local')
22 | || Gate::allows('viewVaporUI', [$request->user()]);
23 |
24 | abort_unless($allowed, 403);
25 |
26 | return $next($request);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Http/Requests/JobMetricRequest.php:
--------------------------------------------------------------------------------
1 | ['string'],
18 | ];
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/Http/Requests/JobRequest.php:
--------------------------------------------------------------------------------
1 | ['nullable', 'string'],
18 | 'startTime' => ['required', 'integer'],
19 | 'cursor' => ['nullable', 'string'],
20 | 'queue' => ['string'],
21 | ];
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Http/Requests/LogRequest.php:
--------------------------------------------------------------------------------
1 | ['nullable', 'string'],
18 | 'startTime' => ['required', 'integer'],
19 | 'cursor' => ['nullable', 'string'],
20 | 'type' => ['nullable', 'string'],
21 | ];
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Jobs/ForgetFailedJob.php:
--------------------------------------------------------------------------------
1 | id = $id;
25 | }
26 |
27 | /**
28 | * Execute the job.
29 | *
30 | * @param FailedJobProviderInterface $provider
31 | * @return void
32 | */
33 | public function handle(FailedJobProviderInterface $provider)
34 | {
35 | if ($job = $provider->find($this->id)) {
36 | $provider->forget($this->id);
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/Jobs/RetryFailedJob.php:
--------------------------------------------------------------------------------
1 | id = $id;
27 | }
28 |
29 | /**
30 | * Execute the job.
31 | *
32 | * @param Factory $queueFactory
33 | * @param FailedJobProviderInterface $provider
34 | * @return void
35 | */
36 | public function handle(Factory $queueFactory, FailedJobProviderInterface $provider)
37 | {
38 | if ($job = $provider->find($this->id)) {
39 | $queueFactory->connection($job->connection)->pushRaw(
40 | $this->resetAttempts($job->payload), $job->queue
41 | );
42 |
43 | $provider->forget($this->id);
44 | }
45 | }
46 |
47 | /**
48 | * Reset the payload attempts.
49 | *
50 | * Applicable to Redis jobs which store attempts in their payload.
51 | *
52 | * @param string $payload
53 | * @return string
54 | */
55 | protected function resetAttempts($payload)
56 | {
57 | $payload = json_decode($payload, true);
58 |
59 | if (isset($payload['attempts'])) {
60 | $payload['attempts'] = 0;
61 | }
62 |
63 | $retryUntil = $payload['retryUntil'] ?? $payload['timeoutAt'] ?? null;
64 |
65 | if ($retryUntil) {
66 | $payload['retryUntil'] = CarbonImmutable::now()->addSeconds((int) ceil(
67 | isset($payload['pushedAt']) ? $retryUntil - $payload['pushedAt'] : config('vapor-ui.retry_until', 3600)
68 | ))->getTimestamp();
69 | }
70 |
71 | return json_encode($payload);
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/Repositories/JobsMetricsRepository.php:
--------------------------------------------------------------------------------
1 | cloudWatch = $cloudWatch;
60 | $this->failedJobs = $failedJobs;
61 | $this->sqs = $sqs;
62 |
63 | $this->queueResolver = function () {
64 | return config('vapor-ui.queue.name');
65 | };
66 | }
67 |
68 | /**
69 | * The total of pending jobs.
70 | *
71 | * @return int
72 | */
73 | public function pending()
74 | {
75 | $prefix = config('vapor-ui.queue.prefix');
76 |
77 | if (empty($prefix)) {
78 | return 0;
79 | }
80 |
81 | $queue = $this->queueResolver->__invoke();
82 |
83 | return collect($this->sqs->getQueueAttributes([
84 | 'AttributeNames' => [
85 | 'ApproximateNumberOfMessages',
86 | 'ApproximateNumberOfMessagesNotVisible',
87 | 'ApproximateNumberOfMessagesDelayed',
88 | ],
89 | 'QueueUrl' => "$prefix/$queue",
90 | ])['Attributes'])->sum();
91 | }
92 |
93 | /**
94 | * The timeseries of the failed jobs in past 24 hours.
95 | *
96 | * @return array
97 | */
98 | public function failedTimeseries()
99 | {
100 | $dayAgo = now()->subHours(23)->setMinute(0);
101 |
102 | return $this->failedJobs()->map(function ($job) {
103 | return Carbon::parse($job->failed_at)
104 | ->setMinute(0)
105 | ->setSecond(0)
106 | ->timestamp;
107 | })->filter(function ($timestamp) use ($dayAgo) {
108 | return Carbon::parse($timestamp)->greaterThan($dayAgo);
109 | })->groupBy(function ($timestamp) {
110 | return $timestamp;
111 | })->union(collect(range(0, 23))->mapWithKeys(function ($hourAgo) {
112 | $hourAgo = now()
113 | ->subHours($hourAgo)
114 | ->setMinute(0)
115 | ->setSecond(0)
116 | ->timestamp;
117 |
118 | return [$hourAgo => collect()];
119 | }))->map(function ($group) {
120 | return $group->count();
121 | })->map(function ($count, $timestamp) {
122 | return [
123 | 'timestamp' => $timestamp,
124 | 'value' => $count,
125 | ];
126 | })->sortBy('timestamp')->values()->all();
127 | }
128 |
129 | /**
130 | * The total of failed jobs in past given hours ago.
131 | *
132 | * @param int $hoursAgo
133 | * @return int
134 | */
135 | public function failedSumByHoursAgo($hoursAgo)
136 | {
137 | $hoursAgo = now()->subHours($hoursAgo);
138 |
139 | return $this->failedJobs()->filter(function ($job) use ($hoursAgo) {
140 | return Carbon::parse($job->failed_at)->greaterThan($hoursAgo);
141 | })->count();
142 | }
143 |
144 | /**
145 | * Gets the average of failed jobs from the given period.
146 | *
147 | * @param int $fromHoursAgo
148 | * @param int $untilHoursAgo
149 | * @return int
150 | */
151 | public function failedAverageByHoursAgo($fromHoursAgo, $untilHoursAgo)
152 | {
153 | $from = now()->subHours($fromHoursAgo);
154 | $until = now()->subHours($untilHoursAgo);
155 |
156 | return (int) ($this->failedJobs()->filter(function ($job) use ($from, $until) {
157 | $failedAt = Carbon::parse($job->failed_at);
158 |
159 | return $failedAt->greaterThanOrEqualTo($from) && $failedAt->lessThan($until);
160 | })->count() / ($fromHoursAgo - $untilHoursAgo));
161 | }
162 |
163 | /**
164 | * The timeseries of the processed jobs in past 24 hours.
165 | *
166 | * @return array
167 | */
168 | public function processedTimeseries()
169 | {
170 | return collect($this->logs([
171 | 'StartTime' => now()->subHours(23)->setMinute(0)->format('Y-m-d H:i:s'),
172 | 'Period' => 60 * 60,
173 | 'EndTime' => now()->addDays(1)->format('Y-m-d H:i:s'),
174 | 'MetricName' => 'NumberOfMessagesReceived',
175 | 'Statistics' => ['Sum'],
176 | ])['Datapoints'])->map(function ($result, $key) {
177 | return [
178 | 'timestamp' => $result['Timestamp']->getTimestamp(),
179 | 'value' => $result['Sum'],
180 | ];
181 | })->sortBy('timestamp')->values()->all();
182 | }
183 |
184 | /**
185 | * The total of processed jobs in past given hours ago.
186 | *
187 | * @param int $hoursAgo
188 | * @return int
189 | */
190 | public function processedSumByHoursAgo($hoursAgo)
191 | {
192 | return (int) collect($this->logs([
193 | 'StartTime' => now()->subHours($hoursAgo)->format('Y-m-d H:i:s'),
194 | 'Period' => 60 * 60 * $hoursAgo,
195 | 'EndTime' => now()->addDays(1)->format('Y-m-d H:i:s'),
196 | 'MetricName' => 'NumberOfMessagesReceived',
197 | 'Statistics' => ['Sum'],
198 | ])['Datapoints'])->pluck('Sum')->sum();
199 | }
200 |
201 | /**
202 | * Gets the average of processed jobs from the given period.
203 | *
204 | * @param int $fromHoursAgo
205 | * @param int $untilHoursAgo
206 | * @return int
207 | */
208 | public function processedAverageByHoursAgo($fromHoursAgo, $untilHoursAgo)
209 | {
210 | return (int) (collect($this->logs([
211 | 'StartTime' => now()->subHours($fromHoursAgo)->format('Y-m-d H:i:s'),
212 | 'EndTime' => now()->subHours($untilHoursAgo)->format('Y-m-d H:i:s'),
213 | 'Period' => 60 * 60 * $fromHoursAgo,
214 | 'MetricName' => 'NumberOfMessagesReceived',
215 | 'Statistics' => ['Sum'],
216 | ])['Datapoints'])->pluck('Sum')->sum() / ($fromHoursAgo - $untilHoursAgo));
217 | }
218 |
219 | /**
220 | * Sets the queue resolver.
221 | *
222 | * @param callable $callback
223 | * @return void
224 | */
225 | public function resolveQueueUsing($callback)
226 | {
227 | $this->queueResolver = $callback;
228 | }
229 |
230 | /**
231 | * Gets the failed jobs.
232 | *
233 | * It memoizes the the `all` call to failed jobs provider.
234 | *
235 | * @return \Illuminate\Support\Collection
236 | */
237 | protected function failedJobs()
238 | {
239 | if ($this->failedJobsCollection === null) {
240 | try {
241 | $this->failedJobsCollection = collect($this->failedJobs->all())
242 | ->filter(function ($content) {
243 | $queue = $this->queueResolver->__invoke();
244 | $prefix = config('vapor-ui.queue.prefix');
245 |
246 | return "$prefix/$queue" == ((array) $content)['queue'];
247 | });
248 | } catch (QueryException $e) {
249 | $this->failedJobsCollection = collect();
250 | }
251 | }
252 |
253 | return $this->failedJobsCollection;
254 | }
255 |
256 | /**
257 | * Performs a request to the Cloud Watch Logs API.
258 | *
259 | * @param array $payload
260 | * @return array
261 | */
262 | protected function logs($payload)
263 | {
264 | return $this->cloudWatch->getMetricStatistics(array_merge_recursive([
265 | 'Dimensions' => [[
266 | 'Name' => 'QueueName', 'Value' => $this->queueResolver->__invoke(),
267 | ]],
268 | 'Namespace' => 'AWS/SQS',
269 | ], $payload))->toArray();
270 | }
271 | }
272 |
--------------------------------------------------------------------------------
/src/Repositories/JobsRepository.php:
--------------------------------------------------------------------------------
1 | provider = $provider;
29 | }
30 |
31 | /**
32 | * Gets the job by the given id.
33 | *
34 | * @param string $group
35 | * @param string $id
36 | * @param array $filters
37 | * @return Job|null
38 | */
39 | public function get($group, $id, $filters = [])
40 | {
41 | if ($content = $this->provider->find($id)) {
42 | return new Job((array) $content, $group, $filters);
43 | }
44 | }
45 |
46 | /**
47 | * Search for the jobs by the given filters.
48 | *
49 | * @param string $group
50 | * @param array $filters
51 | * @return SearchResult
52 | */
53 | public function search($group, $filters)
54 | {
55 | $limit = 50;
56 | $offset = $this->offset($filters);
57 | $startTime = $this->startTime($filters);
58 | $queue = $this->queue($filters);
59 | $queryTerms = $this->queryTerms($filters);
60 |
61 | $all = collect($this->provider->all());
62 |
63 | $entries = $all
64 | ->reverse()
65 | ->filter(function ($content) use ($queue) {
66 | return $queue == ((array) $content)['queue'];
67 | })->filter(function ($content) use ($queryTerms, $startTime) {
68 | foreach ($queryTerms as $term) {
69 | if (! Str::contains(json_encode($content), $term)) {
70 | return false;
71 | }
72 | }
73 |
74 | return (Carbon::parse(
75 | $content->failed_at,
76 | )->timestamp * 1000) >= $startTime;
77 | })->slice($offset, $limit)
78 | ->map(function ($content) use ($group, $filters) {
79 | return new Job((array) $content, $group, $filters);
80 | })->values();
81 |
82 | return new SearchResult(
83 | $entries,
84 | (max($offset, 1) * $limit) < $all->count()
85 | ? (string) ($offset + $limit)
86 | : null
87 | );
88 | }
89 |
90 | /**
91 | * Gets the start time from the given $filters.
92 | *
93 | * @param array $filters
94 | * @return int|null
95 | */
96 | protected function startTime($filters)
97 | {
98 | return isset($filters['startTime']) ? (int) $filters['startTime'] * 1000 : null;
99 | }
100 |
101 | /**
102 | * Gets the queue from the given $filters.
103 | *
104 | * @param array $filters
105 | * @return string
106 | */
107 | protected function queue($filters)
108 | {
109 | $queue = $filters['queue'];
110 | $prefix = config('vapor-ui.queue.prefix');
111 |
112 | return "$prefix/$queue";
113 | }
114 |
115 | /**
116 | * Gets the query from the given $filters.
117 | *
118 | * @param array $filters
119 | * @return array
120 | */
121 | protected function queryTerms($filters)
122 | {
123 | return isset($filters['query']) ? explode(' ', $filters['query']) : [];
124 | }
125 |
126 | /**
127 | * Gets the offset from the given $filters.
128 | *
129 | * @param array $filters
130 | * @return int|null
131 | */
132 | protected function offset($filters)
133 | {
134 | return isset($filters['cursor']) ? (int) $filters['cursor'] : 0;
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/src/Repositories/LogsRepository.php:
--------------------------------------------------------------------------------
1 | client = $client;
59 | }
60 |
61 | /**
62 | * Gets the log by the given id.
63 | *
64 | * @param string $group
65 | * @param string $id
66 | * @param array $filters
67 | * @return Log|null
68 | */
69 | public function get($group, $id, $filters = [])
70 | {
71 | return $this->search($group, $filters)
72 | ->entries
73 | ->firstWhere('id', $id);
74 | }
75 |
76 | /**
77 | * Search for the logs by the given filters.
78 | *
79 | * @param string $group
80 | * @param array $filters
81 | * @return SearchResult
82 | */
83 | public function search($group, $filters = [])
84 | {
85 | try {
86 | $response = $this->client->filterLogEvents(array_filter([
87 | 'logGroupName' => $this->logGroupName($group),
88 | 'limit' => 50,
89 | 'interleaved' => true,
90 | 'nextToken' => $this->nextToken($filters),
91 | 'startTime' => $this->startTime($filters),
92 | 'filterPattern' => $this->filterPattern($filters),
93 | ]))->toArray();
94 | } catch (CloudWatchLogsException $e) {
95 | $resourceNotFoundException = '"__type":"ResourceNotFoundException"';
96 |
97 | if (! Str::contains($e->getMessage(), $resourceNotFoundException)) {
98 | throw $e;
99 | }
100 |
101 | $response = [
102 | 'events' => [],
103 | ];
104 | }
105 |
106 | $entries = collect($response['events'])
107 | ->filter(function ($event) use ($filters) {
108 | return empty($filters['type']) || $filters['type'] === 'timeout' || @json_decode($event['message']);
109 | })->map(function ($event) use ($group, $filters) {
110 | if (array_key_exists('message', $event)
111 | && ($message = json_decode($event['message'], true))) {
112 | $event['message'] = $message;
113 | }
114 |
115 | return new Log($event, $group, $filters);
116 | })->values();
117 |
118 | return new SearchResult($entries, $response['nextToken'] ?? null);
119 | }
120 |
121 | /**
122 | * Returns the log group name from the given $group.
123 | *
124 | * @param string $group
125 | * @return string
126 | */
127 | protected function logGroupName($group)
128 | {
129 | $vaporUi = config('vapor-ui');
130 |
131 | $environment = $vaporUi['environment'];
132 |
133 | $usingDockerRuntime = Str::endsWith(
134 | $_ENV['AWS_LAMBDA_FUNCTION_NAME'] ?? '',
135 | "$environment-d"
136 | );
137 |
138 | return sprintf(
139 | '/aws/lambda/vapor-%s-%s%s%s',
140 | $vaporUi['project'],
141 | $environment,
142 | $usingDockerRuntime ? '-d' : '',
143 | in_array($group, ['cli', 'queue']) ? "-$group" : ''
144 | );
145 | }
146 |
147 | /**
148 | * Gets the next token from the given $filters.
149 | *
150 | * @param array $filters
151 | * @return string|null
152 | */
153 | protected function nextToken($filters)
154 | {
155 | return isset($filters['cursor']) ? (string) $filters['cursor'] : null;
156 | }
157 |
158 | /**
159 | * Gets the start time from the given $filters.
160 | *
161 | * @param array $filters
162 | * @return int|null
163 | */
164 | protected function startTime($filters)
165 | {
166 | return isset($filters['startTime']) ? (int) $filters['startTime'] * 1000 : null;
167 | }
168 |
169 | /**
170 | * Gets the filter pattern from the given $filters.
171 | *
172 | * @param array $filters
173 | * @return string
174 | */
175 | protected function filterPattern($filters)
176 | {
177 | $include = '';
178 |
179 | if (array_key_exists('type', $filters)) {
180 | if ($filters['type'] === 'timeout') {
181 | $include .= '"Task timed out after" ';
182 | } else {
183 | $include .= sprintf('"message" "level_name" "%s" ', strtoupper($filters['type']));
184 | }
185 | }
186 |
187 | $query = $filters['query'] ?? '';
188 |
189 | $exclude = $this->ignore
190 | ? '- "'.collect($this->ignore)->implode('" - "').'"'
191 | : '';
192 |
193 | $filterPattern = empty($query)
194 | ? ''
195 | : collect(explode(' ', $query))
196 | ->map(function ($term) {
197 | return '"'.str_replace('"', '', $term).'"';
198 | })->implode(' ');
199 |
200 | return $include.$filterPattern.' '.$exclude;
201 | }
202 | }
203 |
--------------------------------------------------------------------------------
/src/Support/Cloud.php:
--------------------------------------------------------------------------------
1 | parse(file_get_contents(config('vapor-ui.manifest', base_path('vapor.yml'))));
67 |
68 | $environment = config('vapor-ui.environment');
69 |
70 | $queues = Arr::get(
71 | $vapor,
72 | "environments.$environment.queues",
73 | [config('vapor-ui.queue.name')]
74 | );
75 |
76 | return collect($queues)->map(function ($queue) use ($prefix) {
77 | return str_replace("$prefix/", '', $queue);
78 | })->unique()->values()->all();
79 | }
80 |
81 | /**
82 | * Gets the ssm path from the cloud environment.
83 | *
84 | * @return string
85 | */
86 | public static function ssmPath()
87 | {
88 | $ssmPath = $_ENV['VAPOR_SSM_PATH'] ?? '';
89 |
90 | return $ssmPath;
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/ValueObjects/Job.php:
--------------------------------------------------------------------------------
1 | content = $content;
72 | $this->group = $group;
73 | $this->filters = $filters;
74 |
75 | $this->pullId()
76 | ->pullTimestamp()
77 | ->pullException()
78 | ->pullMessage()
79 | ->normalizePayload();
80 | }
81 |
82 | /**
83 | * Pulls the id from the content.
84 | *
85 | * @return $this
86 | */
87 | protected function pullId()
88 | {
89 | $this->id = $this->content['id'];
90 |
91 | return $this;
92 | }
93 |
94 | /**
95 | * Pulls the timestamp from the content.
96 | *
97 | * @return $this
98 | */
99 | protected function pullTimestamp()
100 | {
101 | $this->timestamp = Carbon::parse(
102 | $this->content['failed_at']
103 | )->timestamp * 1000;
104 |
105 | return $this;
106 | }
107 |
108 | /**
109 | * Pulls the queue exception from the content.
110 | *
111 | * @return $this
112 | */
113 | protected function pullException()
114 | {
115 | if (Str::startsWith($this->content['exception'], ManuallyFailedException::class)) {
116 | $exception = ManuallyFailedException::class;
117 | } else {
118 | [$exception] = explode(':', $this->content['exception']);
119 | }
120 |
121 | if (! empty($exception)) {
122 | $this->exception = $exception;
123 | }
124 |
125 | return $this;
126 | }
127 |
128 | /**
129 | * Pulls the queue message from the content.
130 | *
131 | * @return $this
132 | */
133 | protected function pullMessage()
134 | {
135 | if (Str::startsWith($this->content['exception'], ManuallyFailedException::class)) {
136 | $message = 'Manually failed';
137 | } else {
138 | [$_, $message] = explode(':', $this->content['exception']);
139 | [$message] = explode(' in /', $message);
140 | [$message] = explode(' in closure', $message);
141 | }
142 |
143 | if (! empty($message)) {
144 | $this->message = trim($message);
145 | }
146 |
147 | return $this;
148 | }
149 |
150 | /**
151 | * Normalize the payload.
152 | *
153 | * @return $this
154 | */
155 | protected function normalizePayload()
156 | {
157 | $this->content['payload'] = json_decode($this->content['payload'], true);
158 |
159 | return $this;
160 | }
161 |
162 | /**
163 | * Get the array representation of the job.
164 | *
165 | * @return array
166 | */
167 | #[\ReturnTypeWillChange]
168 | public function jsonSerialize()
169 | {
170 | return (array) $this;
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/src/ValueObjects/Log.php:
--------------------------------------------------------------------------------
1 | content = $content;
78 | $this->group = $group;
79 | $this->filters = $filters;
80 |
81 | $this->pullId()
82 | ->pullTimestamp()
83 | ->pullRequestId()
84 | ->pullType()
85 | ->pullLocation();
86 | }
87 |
88 | /**
89 | * Pulls the id from the content.
90 | *
91 | * @return $this
92 | */
93 | protected function pullId()
94 | {
95 | $this->id = $this->content['eventId'];
96 |
97 | return $this;
98 | }
99 |
100 | /**
101 | * Pulls the timestamp from the content.
102 | *
103 | * @return $this
104 | */
105 | protected function pullTimestamp()
106 | {
107 | $this->timestamp = $this->content['timestamp'];
108 |
109 | return $this;
110 | }
111 |
112 | /**
113 | * Pulls the request id from the content.
114 | *
115 | * @return $this
116 | */
117 | protected function pullRequestId()
118 | {
119 | $this->requestId = Arr::get($this->content, 'message.context.aws_request_id');
120 |
121 | Arr::forget($this->content, 'message.context.aws_request_id');
122 |
123 | return $this;
124 | }
125 |
126 | /**
127 | * Pulls the location from the content.
128 | *
129 | * @return $this
130 | */
131 | protected function pullLocation()
132 | {
133 | $location = Arr::get($this->content, 'message.context.exception.file');
134 |
135 | if ($location) {
136 | $this->location = Str::replaceFirst(
137 | '/var/task/',
138 | '',
139 | $location
140 | );
141 | }
142 |
143 | return $this;
144 | }
145 |
146 | /**
147 | * Pulls the type from the content.
148 | *
149 | * @return $this
150 | */
151 | protected function pullType()
152 | {
153 | if (is_string($this->content['message']) && preg_match('/Task timed out after/', $this->content['message']) > 0) {
154 | $this->type = 'TIMEOUT';
155 | } else {
156 | $this->type = Arr::get($this->content, 'message.level_name', 'RAW');
157 | }
158 |
159 | return $this;
160 | }
161 |
162 | /**
163 | * Get the array representation of the log.
164 | *
165 | * @return array
166 | */
167 | #[\ReturnTypeWillChange]
168 | public function jsonSerialize()
169 | {
170 | return (array) $this;
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/src/ValueObjects/SearchResult.php:
--------------------------------------------------------------------------------
1 | entries = $entries;
34 | $this->cursor = $cursor;
35 | }
36 |
37 | /**
38 | * Get the array representation of the search result.
39 | *
40 | * @return array
41 | */
42 | #[\ReturnTypeWillChange]
43 | public function jsonSerialize()
44 | {
45 | return (array) $this;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/VaporUiServiceProvider.php:
--------------------------------------------------------------------------------
1 | registerRoutes();
27 | $this->loadViewsFrom(__DIR__.'/../resources/views', 'vapor-ui');
28 |
29 | if ($this->app->runningInConsole()) {
30 | $this->publishes([
31 | __DIR__.'/../config/vapor-ui.php' => config_path('vapor-ui.php'),
32 | ], 'vapor-ui-config');
33 |
34 | $this->publishes([
35 | __DIR__.'/../public' => public_path('vendor/vapor-ui'),
36 | ], ['vapor-ui-assets', 'laravel-assets']);
37 |
38 | $this->publishes([
39 | __DIR__.'/../stubs/VaporUiServiceProvider.stub' => app_path('Providers/VaporUiServiceProvider.php'),
40 | ], 'vapor-ui-provider');
41 |
42 | $this->commands([
43 | Console\InstallCommand::class,
44 | Console\PublishCommand::class,
45 | ]);
46 | }
47 | }
48 |
49 | /**
50 | * Register the service provider.
51 | *
52 | * @return void
53 | */
54 | public function register()
55 | {
56 | $this->mergeConfigFrom(__DIR__.'/../config/vapor-ui.php', 'vapor-ui');
57 | $this->mergeConfigFrom(__DIR__.'/../config/runtime.php', 'vapor-ui');
58 |
59 | $this->ensureVaporUiIsConfigured();
60 |
61 | $this->registerClients();
62 | }
63 |
64 | /**
65 | * Binds an implementation of the CloudWatch Client.
66 | *
67 | * @return void
68 | */
69 | public function registerClients()
70 | {
71 | collect([CloudWatchClient::class, CloudWatchLogsClient::class, SqsClient::class])->each(function ($client) {
72 | $this->app->singleton($client, function () use ($client) {
73 | $config = config('vapor-ui');
74 |
75 | $cloudWatchConfig = [
76 | 'region' => $config['region'],
77 | 'version' => 'latest',
78 | ];
79 |
80 | if ($config['key'] && $config['secret']) {
81 | $cloudWatchConfig['credentials'] = Arr::only(
82 | $config, ['key', 'secret', 'token']
83 | );
84 | }
85 |
86 | return new $client($cloudWatchConfig);
87 | });
88 | });
89 | }
90 |
91 | /**
92 | * Register the Vapor UI routes.
93 | *
94 | * @return void
95 | */
96 | protected function registerRoutes()
97 | {
98 | Route::group([
99 | 'domain' => config('vapor-ui.domain', null),
100 | 'prefix' => config('vapor-ui.path', 'vapor-ui'),
101 | ], function () {
102 | $this->loadRoutesFrom(__DIR__.'/../routes/web.php');
103 | });
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/stubs/VaporUiServiceProvider.stub:
--------------------------------------------------------------------------------
1 | gate();
17 | }
18 |
19 | /**
20 | * Register the Vapor UI gate.
21 | *
22 | * This gate determines who can access Vapor UI in non-local environments.
23 | */
24 | protected function gate(): void
25 | {
26 | Gate::define('viewVaporUI', function (User $user = null) {
27 | return in_array(optional($user)->email, [
28 | //
29 | ]);
30 | });
31 | }
32 |
33 | /**
34 | * Register any application services.
35 | */
36 | public function register(): void
37 | {
38 | //
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const defaultTheme = require('tailwindcss/defaultTheme');
2 |
3 | module.exports = {
4 | purge: {
5 | content: ['./resources/views/**/*.blade.php', './resources/js/**/*.vue', './resources/js/**/*.js'],
6 |
7 | options: {
8 | whitelist: [
9 | 'text-green-900',
10 | 'text-green-800',
11 | 'bg-green-100',
12 | 'text-blue-900',
13 | 'text-blue-800',
14 | 'bg-blue-100',
15 | 'text-yellow-900',
16 | 'text-yellow-800',
17 | 'bg-yellow-100',
18 | 'text-red-900',
19 | 'text-red-800',
20 | 'bg-red-100',
21 | 'text-gray-900',
22 | 'text-gray-800',
23 | 'bg-gray-100',
24 | ],
25 | },
26 | },
27 | theme: {
28 | extend: {
29 | fontFamily: {
30 | sans: ['Nunito', ...defaultTheme.fontFamily.sans],
31 | },
32 | },
33 | },
34 | variants: {},
35 | plugins: [require('@tailwindcss/ui')],
36 | };
37 |
--------------------------------------------------------------------------------
/webpack.mix.js:
--------------------------------------------------------------------------------
1 | const mix = require('laravel-mix');
2 | const tailwindcss = require('tailwindcss');
3 | const webpack = require('webpack');
4 |
5 | /*
6 | |--------------------------------------------------------------------------
7 | | Mix Asset Management
8 | |--------------------------------------------------------------------------
9 | |
10 | | Mix provides a clean, fluent API for defining some Webpack build steps
11 | | for your Laravel application. By default, we are compiling the Sass
12 | | file for the application as well as bundling up all the JS files.
13 | |
14 | */
15 |
16 | mix.options({
17 | terser: {
18 | terserOptions: {
19 | compress: {
20 | drop_console: true,
21 | },
22 | },
23 | },
24 | })
25 | .postCss('resources/css/vapor-ui.css', 'public/app.css', [require('postcss-import'), require('tailwindcss')])
26 | .setPublicPath('public')
27 | .js('resources/js/app.js', 'public')
28 | .version()
29 | .webpackConfig({
30 | resolve: {
31 | symlinks: false,
32 | alias: {
33 | '@': path.resolve(__dirname, 'resources/js/'),
34 | },
35 | },
36 | plugins: [new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)],
37 | })
38 | .copy('public', '../vapor-uitest/public/vendor/vapor-ui')
39 | .copy('public', './vendor/orchestra/testbench-core/laravel/public/vendor/vapor-ui');
40 |
--------------------------------------------------------------------------------