├── .devcontainer └── devcontainer.json ├── .editorconfig ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── app ├── Console │ ├── InstallCommand.php │ └── PublishCommand.php ├── Dto │ └── ChartDataPoint.php ├── Http │ ├── Controllers │ │ ├── Controller.php │ │ ├── DashboardController.php │ │ ├── DashboardStatsController.php │ │ └── WorkflowsController.php │ ├── Kernel.php │ ├── Middleware │ │ └── Authenticate.php │ └── Resources │ │ ├── StoredWorkflowExceptionResource.php │ │ ├── StoredWorkflowLogResource.php │ │ └── StoredWorkflowResource.php ├── Providers │ └── WaterlineServiceProvider.stub ├── Repositories │ └── Workflow │ │ ├── Infrastructure │ │ ├── WorkflowRepositoryBaseSQL.php │ │ ├── WorkflowRepositoryMongoDB.php │ │ ├── WorkflowRepositoryMySQL.php │ │ ├── WorkflowRepositoryPostgreSQL.php │ │ ├── WorkflowRepositorySQLServer.php │ │ └── WorkflowRepositorySQLite.php │ │ └── Interfaces │ │ └── WorkflowRepositoryInterface.php ├── Transformer │ └── WorkflowToChartDataTransformer.php ├── Waterline.php ├── WaterlineApplicationServiceProvider.php └── WaterlineServiceProvider.php ├── composer.json ├── config └── waterline.php ├── docker-compose.yml ├── docker ├── Dockerfile ├── create-testing-database-mssql.sh ├── create-testing-database-mysql.sh ├── create-testing-database-pgsql.sql ├── php.ini ├── start-container └── supervisord.conf ├── package.json ├── phpunit-mongo.xml ├── phpunit-mssql.xml ├── phpunit-mysql.xml ├── phpunit-pgsql.xml ├── phpunit-sqlite.xml ├── public ├── app-dark.css ├── app.css ├── app.js ├── app.js.LICENSE.txt ├── img │ ├── favicon.png │ └── sprite.svg └── mix-manifest.json ├── resources ├── img │ └── favicon.png ├── js │ ├── app.js │ ├── base.js │ ├── routes.js │ └── screens │ │ ├── dashboard.vue │ │ └── flows │ │ ├── flow-row.vue │ │ ├── flow.vue │ │ └── index.vue ├── sass │ ├── app-dark.scss │ ├── app.scss │ ├── base.scss │ └── syntaxhighlight.scss └── views │ └── layout.blade.php ├── routes └── web.php ├── tests ├── Feature │ ├── DashboardStatsControllerTest.php │ └── DashboardWorkflowTest.php ├── TestCase.php └── Unit │ └── ExampleTest.php └── webpack.mix.js /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // https://aka.ms/devcontainer.json 2 | { 3 | "name": "Existing Docker Compose (Extend)", 4 | "dockerComposeFile": [ 5 | "../docker-compose.yml" 6 | ], 7 | "service": "laravel.test", 8 | "workspaceFolder": "/var/www/html", 9 | "settings": {}, 10 | "extensions": [ 11 | "editorconfig.editorconfig", 12 | "ryannaddy.laravel-artisan", 13 | "amiralizadeh9480.laravel-extra-intellisense", 14 | "stef-k.laravel-goto-controller", 15 | "codingyu.laravel-goto-view", 16 | "mikestead.dotenv", 17 | "christian-kohler.path-intellisense", 18 | "esbenp.prettier-vscode", 19 | "CoenraadS.bracket-pair-colorizer" 20 | ], 21 | "remoteUser": "sail", 22 | "postCreateCommand": "chown -R 1000:1000 /var/www/html && composer install && php artisan key:generate", 23 | "forwardPorts": [80] 24 | // "runServices": [], 25 | // "shutdownAction": "none", 26 | } 27 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.{yml,yaml}] 15 | indent_size = 2 16 | 17 | [docker-compose.yml] 18 | indent_size = 4 19 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | *.blade.php diff=html 4 | *.css diff=css 5 | *.html diff=html 6 | *.md diff=markdown 7 | *.php diff=php 8 | 9 | /.github export-ignore 10 | CHANGELOG.md export-ignore 11 | .styleci.yml export-ignore 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /public/build 3 | /public/hot 4 | /public/storage 5 | /storage/*.key 6 | /vendor 7 | .env.backup 8 | .env.production 9 | .phpunit.result.cache 10 | Homestead.json 11 | Homestead.yaml 12 | auth.json 13 | npm-debug.log 14 | yarn-error.log 15 | /.fleet 16 | /.idea 17 | /.vscode 18 | .DS_Store 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Richard McDaniel 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 | # Waterline 2 | 3 | An elegant UI for monitoring [Laravel Workflows](https://github.com/laravel-workflow/laravel-workflow). 4 | 5 | ## Installation 6 | 7 | This UI is installable via [Composer](https://getcomposer.org). 8 | 9 | ```bash 10 | composer require laravel-workflow/waterline 11 | 12 | php artisan waterline:install 13 | ``` 14 | 15 | ## Authorization 16 | 17 | Waterline exposes a dashboard at the `/waterline` URL. By default, you will only be able to access this dashboard in the local environment. However, within your `app/Providers/WaterlineServiceProvider.php` file, there is an authorization gate definition. This authorization gate controls access to Waterline in non-local environments. 18 | 19 | ``` 20 | Gate::define('viewWaterline', function ($user) { 21 | return in_array($user->email, [ 22 | 'admin@example.com', 23 | ]); 24 | }); 25 | ``` 26 | 27 | This will allow only the single admin user to access the Waterline UI. 28 | 29 | 30 | ## Upgrading Waterline 31 | 32 | After upgrading Waterline you must publish the latest assets. 33 | 34 | ```bash 35 | composer require laravel-workflow/waterline 36 | 37 | php artisan waterline:publish 38 | ``` 39 | 40 | ## Dashboard View 41 | 42 | ![waterline_dashboard](https://user-images.githubusercontent.com/1130888/202864399-0bf0a3e7-4454-4a30-8fd2-e330b2460b76.png) 43 | 44 | ## Workflow View 45 | 46 | ![workflow](https://user-images.githubusercontent.com/1130888/202864523-edd88fce-0ce9-4e5a-a24c-38afeae4e057.png) 47 | 48 | "Laravel" is a registered trademark of Taylor Otwell. This project is not affiliated, associated, endorsed, or sponsored by Taylor Otwell, nor has it been reviewed, tested, or certified by Taylor Otwell. The use of the trademark "Laravel" is for informational and descriptive purposes only. Laravel Workflow is not officially related to the Laravel trademark or Taylor Otwell. 49 | -------------------------------------------------------------------------------- /app/Console/InstallCommand.php: -------------------------------------------------------------------------------- 1 | comment('Publishing Waterline Service Provider...'); 18 | $this->callSilent('vendor:publish', ['--tag' => 'waterline-provider']); 19 | 20 | $this->comment('Publishing Waterline Assets...'); 21 | $this->callSilent('vendor:publish', ['--tag' => 'waterline-assets']); 22 | 23 | $this->comment('Publishing Waterline Configuration...'); 24 | $this->callSilent('vendor:publish', ['--tag' => 'waterline-config']); 25 | 26 | $this->registerWaterlineServiceProvider(); 27 | 28 | $this->info('Waterline installed successfully.'); 29 | } 30 | 31 | /** 32 | * Register the Waterline service provider in the application configuration file. 33 | * 34 | * @return void 35 | */ 36 | protected function registerWaterlineServiceProvider() 37 | { 38 | $namespace = Str::replaceLast('\\', '', $this->laravel->getNamespace()); 39 | 40 | if (file_exists($this->laravel->bootstrapPath('providers.php'))) { 41 | ServiceProvider::addProviderToBootstrapFile("{$namespace}\\Providers\\WaterlineServiceProvider"); 42 | } else { 43 | $appConfig = file_get_contents(config_path('app.php')); 44 | 45 | if (Str::contains($appConfig, $namespace.'\\Providers\\WaterlineServiceProvider::class')) { 46 | return; 47 | } 48 | 49 | file_put_contents(config_path('app.php'), str_replace( 50 | "{$namespace}\\Providers\EventServiceProvider::class,".PHP_EOL, 51 | "{$namespace}\\Providers\EventServiceProvider::class,".PHP_EOL." {$namespace}\Providers\WaterlineServiceProvider::class,".PHP_EOL, 52 | $appConfig 53 | )); 54 | } 55 | 56 | file_put_contents(app_path('Providers/WaterlineServiceProvider.php'), str_replace( 57 | "namespace App\Providers;", 58 | "namespace {$namespace}\Providers;", 59 | file_get_contents(app_path('Providers/WaterlineServiceProvider.php')) 60 | )); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /app/Console/PublishCommand.php: -------------------------------------------------------------------------------- 1 | call('vendor:publish', [ 31 | '--tag' => 'waterline-assets', 32 | '--force' => true, 33 | ]); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Dto/ChartDataPoint.php: -------------------------------------------------------------------------------- 1 | $this->x, 21 | 'y' => [$this->yMin, $this->yMax], 22 | 'type' => $this->type, 23 | ]; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | middleware(Authenticate::class); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/Http/Controllers/DashboardController.php: -------------------------------------------------------------------------------- 1 | true, 12 | 'cssFile' => true ? 'app-dark.css' : 'app.css', 13 | 'waterlineScriptVariables' => [ 14 | 'path' => config('waterline.path', 'waterline'), 15 | ], 16 | 'isDownForMaintenance' => App::isDownForMaintenance(), 17 | ]); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/Http/Controllers/DashboardStatsController.php: -------------------------------------------------------------------------------- 1 | flowsPastHour(); 15 | 16 | return response()->json([ 17 | 'flows' => $repository->totalFlows(), 18 | 'flows_per_minute' => $flowsPastHour / 60, 19 | 'flows_past_hour' => $flowsPastHour, 20 | 'exceptions_past_hour' => $repository->exceptionsPastHour(), 21 | 'failed_flows_past_week' => $repository->failedFlowsPastWeek(), 22 | 'max_wait_time_workflow' => $repository->maxWaitTimeWorkflow(), 23 | 'max_duration_workflow' => $repository->maxDurationWorkflow(), 24 | 'max_exceptions_workflow' => $repository->maxExceptionsWorkflow(), 25 | ]); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Http/Controllers/WorkflowsController.php: -------------------------------------------------------------------------------- 1 | orderByDesc('id') 15 | ->paginate(50); 16 | } 17 | 18 | public function failed() { 19 | return config('workflows.stored_workflow_model', StoredWorkflow::class)::whereStatus('failed') 20 | ->orderByDesc('id') 21 | ->paginate(50); 22 | } 23 | 24 | public function running() { 25 | return config('workflows.stored_workflow_model', StoredWorkflow::class)::whereIn('status', [ 26 | 'created', 27 | 'pending', 28 | 'running', 29 | 'waiting', 30 | ]) 31 | ->orderByDesc('id') 32 | ->paginate(50); 33 | } 34 | 35 | public function show($id) { 36 | $flow = config('workflows.stored_workflow_model', StoredWorkflow::class)::with(['exceptions', 'logs'])->find($id); 37 | 38 | return StoredWorkflowResource::make($flow); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/Http/Kernel.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | protected $middleware = [ 17 | // \App\Http\Middleware\TrustHosts::class, 18 | \App\Http\Middleware\TrustProxies::class, 19 | \Illuminate\Http\Middleware\HandleCors::class, 20 | \App\Http\Middleware\PreventRequestsDuringMaintenance::class, 21 | \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class, 22 | \App\Http\Middleware\TrimStrings::class, 23 | \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class, 24 | ]; 25 | 26 | /** 27 | * The application's route middleware groups. 28 | * 29 | * @var array> 30 | */ 31 | protected $middlewareGroups = [ 32 | 'web' => [ 33 | \App\Http\Middleware\EncryptCookies::class, 34 | \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, 35 | \Illuminate\Session\Middleware\StartSession::class, 36 | \Illuminate\View\Middleware\ShareErrorsFromSession::class, 37 | \App\Http\Middleware\VerifyCsrfToken::class, 38 | \Illuminate\Routing\Middleware\SubstituteBindings::class, 39 | ], 40 | 41 | 'api' => [ 42 | // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class, 43 | 'throttle:api', 44 | \Illuminate\Routing\Middleware\SubstituteBindings::class, 45 | ], 46 | ]; 47 | 48 | /** 49 | * The application's route middleware. 50 | * 51 | * These middleware may be assigned to groups or used individually. 52 | * 53 | * @var array 54 | */ 55 | protected $routeMiddleware = [ 56 | 'auth' => \App\Http\Middleware\Authenticate::class, 57 | 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class, 58 | 'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class, 59 | 'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class, 60 | 'can' => \Illuminate\Auth\Middleware\Authorize::class, 61 | 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 62 | 'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class, 63 | 'signed' => \App\Http\Middleware\ValidateSignature::class, 64 | 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 65 | 'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class, 66 | ]; 67 | } 68 | -------------------------------------------------------------------------------- /app/Http/Middleware/Authenticate.php: -------------------------------------------------------------------------------- 1 | exception; 22 | 23 | $unserialized = Serializer::unserialize($exception); 24 | if (is_array($unserialized) 25 | && array_key_exists('class', $unserialized) 26 | && is_subclass_of($unserialized['class'], \Throwable::class) 27 | && file_exists($unserialized['file']) 28 | ) { 29 | $file = new SplFileObject($unserialized['file']); 30 | $file->seek($unserialized['line'] - 4); 31 | for ($line = 0; $line < 7; ++$line) { 32 | $code .= $file->current(); 33 | $file->next(); 34 | if ($file->eof()) break; 35 | } 36 | $code = rtrim($code); 37 | $exception = serialize($unserialized); 38 | } 39 | 40 | return [ 41 | "id" => $this->id, 42 | "code" => $code, 43 | "exception" => $exception, 44 | "class" => $this->class, 45 | "created_at" => $this->created_at, 46 | ]; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /app/Http/Resources/StoredWorkflowLogResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 21 | "index" => $this->index, 22 | "now" => $this->now, 23 | "class" => $this->class, 24 | "result" => serialize(Serializer::unserialize($this->result)), 25 | "created_at" => $this->created_at, 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Http/Resources/StoredWorkflowResource.php: -------------------------------------------------------------------------------- 1 | $this->id, 22 | "class" => $this->class, 23 | "arguments" => serialize(Serializer::unserialize($this->arguments)), 24 | "output" => $this->output === null ? serialize(null) : serialize(Serializer::unserialize($this->output)), 25 | "status" => $this->status, 26 | "created_at" => $this->created_at, 27 | "updated_at" => $this->updated_at, 28 | "logs" => StoredWorkflowLogResource::collection($this->logs), 29 | "exceptions" => StoredWorkflowExceptionResource::collection($this->exceptions), 30 | "chartData" => app(WorkflowToChartDataTransformer::class)->transform($this->resource), 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Providers/WaterlineServiceProvider.stub: -------------------------------------------------------------------------------- 1 | email, [ 21 | // 22 | ]); 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Repositories/Workflow/Infrastructure/WorkflowRepositoryBaseSQL.php: -------------------------------------------------------------------------------- 1 | workflowModel = config('workflows.stored_workflow_model', \Workflow\Models\StoredWorkflow::class); 15 | $this->workflowExceptionModel = config('workflows.stored_workflow_exception_model', \Workflow\Models\StoredWorkflowException::class); 16 | } 17 | 18 | public function flowsPastHour(): int 19 | { 20 | return $this->workflowModel::where('updated_at', '>=', now()->subHour())->count(); 21 | } 22 | 23 | public function exceptionsPastHour(): int 24 | { 25 | return $this->workflowExceptionModel::where('created_at', '>=', now()->subHour())->count(); 26 | } 27 | 28 | public function failedFlowsPastWeek(): int 29 | { 30 | return $this->workflowModel::where('status', 'failed') 31 | ->where('updated_at', '>=', now()->subDays(7)) 32 | ->count(); 33 | } 34 | 35 | public function maxWaitTimeWorkflow() 36 | { 37 | return $this->workflowModel::where('status', 'pending') 38 | ->orderBy('updated_at') 39 | ->first(); 40 | } 41 | 42 | public function maxExceptionsWorkflow() 43 | { 44 | return $this->workflowModel::withCount('exceptions') 45 | ->has('exceptions') 46 | ->orderByDesc('exceptions_count') 47 | ->orderByDesc('updated_at') 48 | ->first(); 49 | } 50 | 51 | public function totalFlows(): int 52 | { 53 | return $this->workflowModel::count(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/Repositories/Workflow/Infrastructure/WorkflowRepositoryMongoDB.php: -------------------------------------------------------------------------------- 1 | workflowModel::where('status', 'pending') 10 | ->orderBy('updated_at') 11 | ->first(); 12 | 13 | if ($maxWaitTimeWorkflow && $maxWaitTimeWorkflow->_id) { 14 | $maxWaitTimeWorkflow->id = $maxWaitTimeWorkflow->_id; 15 | } 16 | 17 | return $maxWaitTimeWorkflow; 18 | } 19 | 20 | public function maxDurationWorkflow() 21 | { 22 | $maxDurationWorkflow = $this->workflowModel::select('*') 23 | ->raw(function ($collection) { 24 | return $collection->aggregate([ 25 | [ 26 | '$match' => [ 27 | 'status' => [ '$ne' => 'pending' ] 28 | ] 29 | ], 30 | [ 31 | '$addFields' => [ 32 | 'duration' => [ 33 | '$subtract' => [ 34 | ['$toDate' => '$updated_at'], 35 | ['$toDate' => '$created_at'] 36 | ] 37 | ] 38 | ] 39 | ], 40 | [ 41 | '$sort' => ['duration' => -1] 42 | ], 43 | [ 44 | '$limit' => 1 45 | ] 46 | ]); 47 | }) 48 | ->first(); 49 | 50 | if ($maxDurationWorkflow) { 51 | $maxDurationWorkflow->id = $maxDurationWorkflow->_id; 52 | } 53 | 54 | return $maxDurationWorkflow; 55 | } 56 | 57 | public function maxExceptionsWorkflow() 58 | { 59 | $maxExceptionsWorkflow = $this->workflowExceptionModel::raw(function ($collection) { 60 | return $collection->aggregate([ 61 | ['$group' => ['_id' => '$stored_workflow_id', 'count' => ['$sum' => 1]]], 62 | ['$sort' => ['count' => -1]], 63 | ['$limit' => 1] 64 | ]); 65 | })->first(); 66 | 67 | if ($maxExceptionsWorkflow) { 68 | $mostExceptionWorkflowId = $maxExceptionsWorkflow['_id']; 69 | 70 | $maxExceptionsWorkflow = $this->workflowModel::where('_id', $mostExceptionWorkflowId)->first(); 71 | 72 | if ($maxExceptionsWorkflow) { 73 | $maxExceptionsWorkflow->exceptions_count = $this->workflowExceptionModel::where('stored_workflow_id', $mostExceptionWorkflowId)->count(); 74 | $maxExceptionsWorkflow->id = $maxExceptionsWorkflow->_id; 75 | } 76 | } 77 | 78 | return $maxExceptionsWorkflow; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /app/Repositories/Workflow/Infrastructure/WorkflowRepositoryMySQL.php: -------------------------------------------------------------------------------- 1 | workflowModel::select('*') 10 | ->selectRaw('TIMEDIFF(created_at, updated_at) as duration') 11 | ->where('status', '!=', 'pending') 12 | ->orderByDesc('duration') 13 | ->first(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/Repositories/Workflow/Infrastructure/WorkflowRepositoryPostgreSQL.php: -------------------------------------------------------------------------------- 1 | workflowModel::select('*') 10 | ->selectRaw('(EXTRACT(EPOCH FROM created_at - updated_at)) as duration') 11 | ->where('status', '!=', 'pending') 12 | ->orderByDesc('duration') 13 | ->first(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/Repositories/Workflow/Infrastructure/WorkflowRepositorySQLServer.php: -------------------------------------------------------------------------------- 1 | workflowModel::select('*') 10 | ->selectRaw('DATEDIFF(SECOND, created_at, updated_at) as duration') 11 | ->where('status', '!=', 'pending') 12 | ->orderByDesc('duration') 13 | ->first(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/Repositories/Workflow/Infrastructure/WorkflowRepositorySQLite.php: -------------------------------------------------------------------------------- 1 | workflowModel::select('*') 10 | ->selectRaw('julianday(created_at) - julianday(updated_at) as duration') 11 | ->where('status', '!=', 'pending') 12 | ->orderByDesc('duration') 13 | ->first(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/Repositories/Workflow/Interfaces/WorkflowRepositoryInterface.php: -------------------------------------------------------------------------------- 1 | transformWorkflow($storedWorkflow); 21 | 22 | foreach ($storedWorkflow->children as $childWorkflow) { 23 | $data = array_merge( 24 | $data, 25 | $this->transform($childWorkflow), 26 | ); 27 | } 28 | 29 | return $data; 30 | } 31 | 32 | /** 33 | * @return ChartDataPoint[] 34 | */ 35 | private function transformWorkflow(StoredWorkflow $storedWorkflow) : array { 36 | $data = [ 37 | app(ChartDataPoint::class, [ 38 | 'x' => $storedWorkflow->class, 39 | 'yMin' => (float) $storedWorkflow->created_at->isoFormat('XSSS'), 40 | 'yMax' => (float) $storedWorkflow->updated_at->isoFormat('XSSS'), 41 | 'type' => 'Workflow' 42 | ]) 43 | ]; 44 | 45 | $prevLogCreated = $storedWorkflow->created_at; 46 | foreach ($storedWorkflow->logs as $log) { 47 | if (is_subclass_of($log->class, Workflow::class)) { 48 | continue; 49 | } 50 | 51 | $data[] = $this->transformLog($log, $prevLogCreated); 52 | $prevLogCreated = $log->created_at; 53 | } 54 | 55 | return $data; 56 | } 57 | 58 | /** 59 | * @return ChartDataPoint 60 | */ 61 | private function transformLog(StoredWorkflowLog $storedWorkflowLog, CarbonInterface $prevLogCreated) : ChartDataPoint { 62 | 63 | return app(ChartDataPoint::class, [ 64 | 'x' => $storedWorkflowLog->class, 65 | 'yMin' => (float) $prevLogCreated->isoFormat('XSSS'), 66 | 'yMax' => (float) $storedWorkflowLog->created_at->isoFormat('XSSS'), 67 | 'type' => 'Activity', 68 | ]); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/Waterline.php: -------------------------------------------------------------------------------- 1 | environment('local'); 17 | })($request); 18 | } 19 | 20 | public static function auth(Closure $callback) 21 | { 22 | static::$authUsing = $callback; 23 | 24 | return new static; 25 | } 26 | 27 | public static function assetsAreCurrent() 28 | { 29 | $publishedPath = public_path('vendor/waterline/mix-manifest.json'); 30 | 31 | if (! File::exists($publishedPath)) { 32 | throw new RuntimeException('Waterline assets are not published. Please run: php artisan waterline:publish'); 33 | } 34 | 35 | return File::get($publishedPath) === File::get(__DIR__.'/../public/mix-manifest.json'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/WaterlineApplicationServiceProvider.php: -------------------------------------------------------------------------------- 1 | authorization(); 22 | } 23 | 24 | protected function authorization() 25 | { 26 | $this->gate(); 27 | 28 | Waterline::auth(function ($request) { 29 | return Gate::check('viewWaterline', [$request->user()]) || app()->environment('local'); 30 | }); 31 | } 32 | 33 | protected function gate() 34 | { 35 | Gate::define('viewWaterline', function ($user) { 36 | return in_array($user->email, [ 37 | // 38 | ]); 39 | }); 40 | } 41 | 42 | public function register() 43 | { 44 | if (! class_exists('Workflow\Models\Model')) { 45 | class_alias(config('workflows.base_model', Model::class), 'Workflow\Models\Model'); 46 | } 47 | 48 | $drivers = [ 49 | 'mongodb' => WorkflowRepositoryMongoDB::class, 50 | 'mysql' => WorkflowRepositoryMySQL::class, 51 | 'pgsql' => WorkflowRepositoryPostgreSQL::class, 52 | 'sqlite' => WorkflowRepositorySQLite::class, 53 | 'sqlsrv' => WorkflowRepositorySQLServer::class, 54 | ]; 55 | 56 | $driver = DB::connection((new (config('workflows.stored_workflow_model', StoredWorkflow::class)))->getConnectionName())->getDriverName(); 57 | 58 | $this->app->bind(WorkflowRepositoryInterface::class, $drivers[$driver] ?? WorkflowRepositoryMySQL::class); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/WaterlineServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerRoutes(); 18 | $this->registerResources(); 19 | $this->defineAssetPublishing(); 20 | $this->offerPublishing(); 21 | $this->registerCommands(); 22 | } 23 | 24 | /** 25 | * Register the Waterline routes. 26 | * 27 | * @return void 28 | */ 29 | protected function registerRoutes() 30 | { 31 | Route::group([ 32 | 'domain' => config('waterline.domain', null), 33 | 'prefix' => config('waterline.path', 'waterline'), 34 | 'namespace' => 'Waterline\Http\Controllers', 35 | 'middleware' => config('waterline.middleware', 'web'), 36 | ], function () { 37 | $this->loadRoutesFrom(__DIR__.'/../routes/web.php'); 38 | }); 39 | } 40 | 41 | /** 42 | * Register the Waterline resources. 43 | * 44 | * @return void 45 | */ 46 | protected function registerResources() 47 | { 48 | $this->loadViewsFrom(__DIR__.'/../resources/views', 'waterline'); 49 | } 50 | 51 | /** 52 | * Define the asset publishing configuration. 53 | * 54 | * @return void 55 | */ 56 | public function defineAssetPublishing() 57 | { 58 | $this->publishes([ 59 | WATERLINE_PATH.'/public' => public_path('vendor/waterline'), 60 | ], ['waterline-assets', 'laravel-assets']); 61 | } 62 | 63 | /** 64 | * Setup the resource publishing groups for Waterline. 65 | * 66 | * @return void 67 | */ 68 | protected function offerPublishing() 69 | { 70 | if ($this->app->runningInConsole()) { 71 | $this->publishes([ 72 | __DIR__.'/Providers/WaterlineServiceProvider.stub' => app_path('Providers/WaterlineServiceProvider.php'), 73 | ], 'waterline-provider'); 74 | 75 | $this->publishes([ 76 | __DIR__.'/../config/waterline.php' => config_path('waterline.php'), 77 | ], 'waterline-config'); 78 | } 79 | } 80 | 81 | /** 82 | * Register the Waterline Artisan commands. 83 | * 84 | * @return void 85 | */ 86 | protected function registerCommands() 87 | { 88 | if ($this->app->runningInConsole()) { 89 | $this->commands([ 90 | Console\InstallCommand::class, 91 | Console\PublishCommand::class, 92 | ]); 93 | } 94 | } 95 | 96 | /** 97 | * Register any application services. 98 | * 99 | * @return void 100 | */ 101 | public function register() 102 | { 103 | if (! defined('WATERLINE_PATH')) { 104 | define('WATERLINE_PATH', realpath(__DIR__.'/../')); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravel-workflow/waterline", 3 | "description": "An elegant UI for monitoring Laravel Workflows.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Richard McDaniel", 8 | "email": "richard.lee.mcdaniel@gmail.com" 9 | } 10 | ], 11 | "require": { 12 | "php": "^8.0.2", 13 | "illuminate/support": "^9.0|^10.0|^11.0|^12.0", 14 | "laravel-workflow/laravel-workflow": "^1.0" 15 | }, 16 | "require-dev": { 17 | "fakerphp/faker": "^1.9.1", 18 | "mockery/mockery": "^1.4.4", 19 | "mongodb/laravel-mongodb": "^3.9", 20 | "mongodb/mongodb": "1.11", 21 | "orchestra/testbench": "^7.29", 22 | "phpunit/phpunit": "^9.5.10" 23 | }, 24 | "autoload": { 25 | "psr-4": { 26 | "Waterline\\": "app/" 27 | } 28 | }, 29 | "autoload-dev": { 30 | "psr-4": { 31 | "Waterline\\Tests\\": "tests/" 32 | } 33 | }, 34 | "scripts": { 35 | "test": "composer test-mongo && composer test-mssql && composer test-mysql && composer test-pgsql && composer test-sqlite", 36 | "test-mongo": "vendor/bin/phpunit --testdox --configuration=phpunit-mongo.xml", 37 | "test-mssql": "vendor/bin/phpunit --testdox --configuration=phpunit-mssql.xml", 38 | "test-mysql": "vendor/bin/phpunit --testdox --configuration=phpunit-mysql.xml", 39 | "test-pgsql": "vendor/bin/phpunit --testdox --configuration=phpunit-pgsql.xml", 40 | "test-sqlite": "vendor/bin/phpunit --testdox --configuration=phpunit-sqlite.xml" 41 | }, 42 | "extra": { 43 | "laravel": { 44 | "providers": [ 45 | "Waterline\\WaterlineServiceProvider" 46 | ], 47 | "aliases": { 48 | "Waterline": "Waterline\\Waterline" 49 | } 50 | } 51 | }, 52 | "config": { 53 | "optimize-autoloader": true, 54 | "preferred-install": "dist", 55 | "sort-packages": true, 56 | "allow-plugins": { 57 | "pestphp/pest-plugin": true 58 | } 59 | }, 60 | "minimum-stability": "dev", 61 | "prefer-stable": true 62 | } 63 | -------------------------------------------------------------------------------- /config/waterline.php: -------------------------------------------------------------------------------- 1 | env('WATERLINE_DOMAIN'), 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Waterline Path 21 | |-------------------------------------------------------------------------- 22 | | 23 | | This is the URI path where Waterline will be accessible from. Feel free 24 | | to change this path to anything you like. Note that the URI will not 25 | | affect the paths of its internal API that aren't exposed to users. 26 | | 27 | */ 28 | 29 | 'path' => env('WATERLINE_PATH', 'waterline'), 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Waterline Route Middleware 34 | |-------------------------------------------------------------------------- 35 | | 36 | | These middleware will get attached onto each Waterline route, giving you 37 | | the chance to add your own middleware to this list or change any of 38 | | the existing middleware. Or, you can simply stick with this list. 39 | | 40 | */ 41 | 42 | 'middleware' => ['web'], 43 | ]; 44 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | # For more information: https://laravel.com/docs/sail 2 | version: '3' 3 | services: 4 | laravel.test: 5 | build: 6 | context: ./docker 7 | dockerfile: Dockerfile 8 | args: 9 | WWWGROUP: '1000' 10 | image: sail-8.2/app 11 | extra_hosts: 12 | - 'host.docker.internal:host-gateway' 13 | ports: 14 | - '${APP_PORT:-80}:80' 15 | - '${VITE_PORT:-5173}:${VITE_PORT:-5173}' 16 | environment: 17 | WWWUSER: '1000' 18 | LARAVEL_SAIL: 1 19 | XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}' 20 | XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}' 21 | volumes: 22 | - '.:/var/www/html' 23 | networks: 24 | - sail 25 | depends_on: 26 | - mysql 27 | - redis 28 | mongo: 29 | image: 'mongo:latest' 30 | ports: 31 | - '${FORWARD_DB_PORT:-27017}:27017' 32 | environment: 33 | MONGO_INITDB_ROOT_USERNAME: 'testing' 34 | MONGO_INITDB_ROOT_PASSWORD: 'password' 35 | MONGO_INITDB_DATABASE: 'testing' 36 | volumes: 37 | - 'sail-mongo:/data/db' 38 | # - './docker/create-testing-database-mongo.js:/docker-entrypoint-initdb.d/10-create-testing-database.js' 39 | networks: 40 | - sail 41 | healthcheck: 42 | test: ["CMD", "mongo", "--eval", "db.adminCommand('ping')"] 43 | retries: 3 44 | timeout: 5s 45 | mssql: 46 | image: 'mcr.microsoft.com/mssql/server:2022-latest' 47 | ports: 48 | - '${FORWARD_DB_PORT:-1433}:1433' 49 | environment: 50 | ACCEPT_EULA: 'Y' 51 | MSSQL_SA_PASSWORD: 'P@ssword' 52 | volumes: 53 | - 'sail-mssql:/var/lib/mssql/data' 54 | - './docker/create-testing-database-mssql.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh' 55 | networks: 56 | - sail 57 | healthcheck: 58 | test: ["CMD", "/opt/mssql-tools/bin/sqlcmd", "-S", "localhost", "-U", "sa", "-P", "P@ssword", "-Q", "SELECT 1", "-b", "-o", "/dev/null"] 59 | interval: 10s 60 | timeout: 3s 61 | retries: 10 62 | start_period: 10s 63 | mysql: 64 | image: 'mysql/mysql-server:8.0' 65 | ports: 66 | - '${FORWARD_DB_PORT:-3306}:3306' 67 | environment: 68 | MYSQL_ROOT_PASSWORD: 'password' 69 | MYSQL_ROOT_HOST: "%" 70 | MYSQL_DATABASE: 'testing' 71 | MYSQL_USER: 'testing' 72 | MYSQL_PASSWORD: 'password' 73 | MYSQL_ALLOW_EMPTY_PASSWORD: 1 74 | volumes: 75 | - 'sail-mysql:/var/lib/mysql' 76 | - './docker/create-testing-database-mysql.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh' 77 | networks: 78 | - sail 79 | healthcheck: 80 | test: ["CMD", "mysqladmin", "ping", "-ppassword"] 81 | retries: 3 82 | timeout: 5s 83 | pgsql: 84 | image: 'postgres:15' 85 | ports: 86 | - '${FORWARD_DB_PORT:-5432}:5432' 87 | environment: 88 | PGPASSWORD: 'password' 89 | POSTGRES_DB: 'testing' 90 | POSTGRES_USER: 'testing' 91 | POSTGRES_PASSWORD: 'password' 92 | volumes: 93 | - 'sail-pgsql:/var/lib/postgresql/data' 94 | - './docker/create-testing-database-pgsql.sql:/docker-entrypoint-initdb.d/10-create-testing-database.sql' 95 | networks: 96 | - sail 97 | healthcheck: 98 | test: ["CMD", "pg_isready", "-q", "-d", "testing", "-U", "testing"] 99 | retries: 3 100 | timeout: 5s 101 | redis: 102 | image: 'redis:alpine' 103 | ports: 104 | - '${FORWARD_REDIS_PORT:-6379}:6379' 105 | volumes: 106 | - 'sail-redis:/data' 107 | networks: 108 | - sail 109 | healthcheck: 110 | test: ["CMD", "redis-cli", "ping"] 111 | retries: 3 112 | timeout: 5s 113 | networks: 114 | sail: 115 | driver: bridge 116 | volumes: 117 | sail-mongo: 118 | driver: local 119 | sail-mssql: 120 | driver: local 121 | sail-mysql: 122 | driver: local 123 | sail-pgsql: 124 | driver: local 125 | sail-redis: 126 | driver: local 127 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:22.04 2 | 3 | LABEL maintainer="Taylor Otwell" 4 | 5 | ARG WWWGROUP 6 | ARG NODE_VERSION=18 7 | ARG POSTGRES_VERSION=15 8 | 9 | WORKDIR /var/www/html 10 | 11 | ENV DEBIAN_FRONTEND noninteractive 12 | ENV TZ=UTC 13 | 14 | RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone 15 | 16 | RUN apt-get update \ 17 | && apt-get install -y gnupg gosu curl ca-certificates zip unzip git supervisor sqlite3 libcap2-bin libpng-dev python2 dnsutils \ 18 | && curl -sS 'https://keyserver.ubuntu.com/pks/lookup?op=get&search=0x14aa40ec0831756756d7f66c4f4ea0aae5267a6c' | gpg --dearmor | tee /etc/apt/keyrings/ppa_ondrej_php.gpg > /dev/null \ 19 | && echo "deb [signed-by=/etc/apt/keyrings/ppa_ondrej_php.gpg] https://ppa.launchpadcontent.net/ondrej/php/ubuntu jammy main" > /etc/apt/sources.list.d/ppa_ondrej_php.list \ 20 | && apt-get update \ 21 | && apt-get install -y php8.2-cli php8.2-dev \ 22 | php8.2-pgsql php8.2-sqlite3 php8.2-gd php8.2-imagick \ 23 | php8.2-curl \ 24 | php8.2-imap php8.2-mysql php8.2-mbstring \ 25 | php8.2-xml php8.2-zip php8.2-bcmath php8.2-soap \ 26 | php8.2-intl php8.2-readline \ 27 | php8.2-ldap php8.2-mongodb \ 28 | php8.2-msgpack php8.2-igbinary php8.2-redis php8.2-swoole \ 29 | php8.2-memcached php8.2-pcov php8.2-xdebug \ 30 | && curl -sLS https://getcomposer.org/installer | php -- --install-dir=/usr/bin/ --filename=composer \ 31 | && curl -sLS https://deb.nodesource.com/setup_$NODE_VERSION.x | bash - \ 32 | && apt-get install -y nodejs \ 33 | && npm install -g npm \ 34 | && curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | gpg --dearmor | tee /etc/apt/keyrings/yarn.gpg >/dev/null \ 35 | && echo "deb [signed-by=/etc/apt/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list \ 36 | && curl -sS https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /etc/apt/keyrings/pgdg.gpg >/dev/null \ 37 | && echo "deb [signed-by=/etc/apt/keyrings/pgdg.gpg] http://apt.postgresql.org/pub/repos/apt jammy-pgdg main" > /etc/apt/sources.list.d/pgdg.list \ 38 | && apt-get update \ 39 | && apt-get install -y yarn \ 40 | && apt-get install -y mysql-client \ 41 | && apt-get install -y postgresql-client-$POSTGRES_VERSION \ 42 | && apt-get install -y unixodbc-dev unixodbc odbcinst \ 43 | && curl -sSL https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor | tee /etc/apt/keyrings/microsoft.gpg >/dev/null \ 44 | && echo "deb [signed-by=/etc/apt/keyrings/microsoft.gpg] https://packages.microsoft.com/ubuntu/22.04/prod jammy main" | tee /etc/apt/sources.list.d/mssql-release.list \ 45 | && apt-get update \ 46 | && ACCEPT_EULA=Y apt-get install -y msodbcsql17 mssql-tools \ 47 | && echo 'export PATH="$PATH:/opt/mssql-tools/bin"' >> ~/.bashrc \ 48 | && echo 'export PATH="$PATH:/opt/mssql-tools/bin"' >> /etc/profile.d/mssql.sh \ 49 | && apt-get install -y php-pear php8.2-xml php8.2-dev \ 50 | && pecl channel-update pecl.php.net \ 51 | && pecl install sqlsrv \ 52 | && pecl install pdo_sqlsrv \ 53 | && echo "extension=sqlsrv.so" > /etc/php/8.2/mods-available/sqlsrv.ini \ 54 | && echo "extension=pdo_sqlsrv.so" > /etc/php/8.2/mods-available/pdo_sqlsrv.ini \ 55 | && phpenmod sqlsrv pdo_sqlsrv \ 56 | # Ensure PHP 8.3 is not installed 57 | && apt-get remove -y php8.3-cli php8.3-dev || true \ 58 | && apt-get -y autoremove \ 59 | && apt-get clean \ 60 | && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* 61 | 62 | RUN setcap "cap_net_bind_service=+ep" /usr/bin/php8.2 63 | 64 | RUN groupadd --force -g 1000 sail 65 | RUN useradd -ms /bin/bash --no-user-group -g 1000 -u 1337 sail 66 | 67 | COPY start-container /usr/local/bin/start-container 68 | COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf 69 | COPY php.ini /etc/php/8.2/cli/conf.d/99-sail.ini 70 | RUN chmod +x /usr/local/bin/start-container 71 | 72 | EXPOSE 8000 73 | 74 | ENTRYPOINT ["start-container"] 75 | -------------------------------------------------------------------------------- /docker/create-testing-database-mssql.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | /opt/mssql-tools/bin/sqlcmd -S "mssql" -U "sa" -P "P@ssword" -Q "CREATE DATABASE testing" 4 | -------------------------------------------------------------------------------- /docker/create-testing-database-mysql.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | mysql --user=root --password="$MYSQL_ROOT_PASSWORD" <<-EOSQL 4 | CREATE DATABASE IF NOT EXISTS testing; 5 | GRANT ALL PRIVILEGES ON \`testing%\`.* TO '$MYSQL_USER'@'%'; 6 | EOSQL 7 | -------------------------------------------------------------------------------- /docker/create-testing-database-pgsql.sql: -------------------------------------------------------------------------------- 1 | SELECT 'CREATE DATABASE testing' 2 | WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'testing')\gexec 3 | -------------------------------------------------------------------------------- /docker/php.ini: -------------------------------------------------------------------------------- 1 | [PHP] 2 | post_max_size = 100M 3 | upload_max_filesize = 100M 4 | variables_order = EGPCS 5 | 6 | [opcache] 7 | opcache.enable_cli=1 8 | -------------------------------------------------------------------------------- /docker/start-container: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | if [ ! -z "$WWWUSER" ]; then 4 | usermod -u $WWWUSER sail 5 | fi 6 | 7 | if [ ! -d /.composer ]; then 8 | mkdir /.composer 9 | fi 10 | 11 | chmod -R ugo+rw /.composer 12 | 13 | if [ $# -gt 0 ]; then 14 | exec gosu $WWWUSER "$@" 15 | else 16 | exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf 17 | fi 18 | -------------------------------------------------------------------------------- /docker/supervisord.conf: -------------------------------------------------------------------------------- 1 | [supervisord] 2 | nodaemon=true 3 | user=root 4 | logfile=/var/log/supervisor/supervisord.log 5 | pidfile=/var/run/supervisord.pid 6 | 7 | [program:php] 8 | command=/usr/bin/php -d variables_order=EGPCS /var/www/html/artisan serve --host=0.0.0.0 --port=80 9 | user=sail 10 | environment=LARAVEL_SAIL="1" 11 | stdout_logfile=/dev/stdout 12 | stdout_logfile_maxbytes=0 13 | stderr_logfile=/dev/stderr 14 | stderr_logfile_maxbytes=0 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "npm run development", 5 | "development": "mix", 6 | "watch": "mix watch", 7 | "watch-poll": "mix watch -- --watch-options-poll=1000", 8 | "hot": "mix watch --hot", 9 | "prod": "npm run production", 10 | "production": "mix --production" 11 | }, 12 | "devDependencies": { 13 | "apexcharts": "^3.36.1", 14 | "axios": "^0.21.1", 15 | "bootstrap": "^4.3.1", 16 | "chart.js": "^2.5.0", 17 | "highlight.js": "^10.4.1", 18 | "jquery": "^3.5.0", 19 | "laravel-mix": "^6.0.13", 20 | "lodash": "^4.17.19", 21 | "md5": "^2.2.1", 22 | "moment": "^2.29.4", 23 | "moment-timezone": "^0.5.35", 24 | "phpunserialize": "1.*", 25 | "popper.js": "^1.12", 26 | "prismjs": "^1.29.0", 27 | "resolve-url-loader": "^3.1.2", 28 | "sass": "^1.26.3", 29 | "sass-loader": "^11.0.1", 30 | "sql-formatter": "^4.0.2", 31 | "sweetalert2": "11.4.8", 32 | "vue": "^2.5.7", 33 | "vue-apexcharts": "^1.6.2", 34 | "vue-json-pretty": "^1.4.1", 35 | "vue-loader": "^15.9.6", 36 | "vue-prism-editor": "^1.3.0", 37 | "vue-router": "^3.0.1", 38 | "vue-template-compiler": "^2.5.21" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /phpunit-mongo.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests/Unit 10 | 11 | 12 | ./tests/Feature 13 | 14 | 15 | 16 | 17 | ./app 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /phpunit-mssql.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests/Unit 10 | 11 | 12 | ./tests/Feature 13 | 14 | 15 | 16 | 17 | ./app 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /phpunit-mysql.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests/Unit 10 | 11 | 12 | ./tests/Feature 13 | 14 | 15 | 16 | 17 | ./app 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /phpunit-pgsql.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests/Unit 10 | 11 | 12 | ./tests/Feature 13 | 14 | 15 | 16 | 17 | ./app 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /phpunit-sqlite.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | ./tests/Unit 10 | 11 | 12 | ./tests/Feature 13 | 14 | 15 | 16 | 17 | ./app 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /public/app.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v4.6.2 (https://getbootstrap.com/) 3 | * Copyright 2011-2022 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 5 | */ 6 | 7 | /*! 8 | * ApexCharts v3.36.1 9 | * (c) 2018-2022 ApexCharts 10 | * Released under the MIT License. 11 | */ 12 | 13 | /*! 14 | * Sizzle CSS Selector Engine v2.3.6 15 | * https://sizzlejs.com/ 16 | * 17 | * Copyright JS Foundation and other contributors 18 | * Released under the MIT license 19 | * https://js.foundation/ 20 | * 21 | * Date: 2021-02-16 22 | */ 23 | 24 | /*! 25 | * Vue.js v2.7.13 26 | * (c) 2014-2022 Evan You 27 | * Released under the MIT License. 28 | */ 29 | 30 | /*! 31 | * jQuery JavaScript Library v3.6.1 32 | * https://jquery.com/ 33 | * 34 | * Includes Sizzle.js 35 | * https://sizzlejs.com/ 36 | * 37 | * Copyright OpenJS Foundation and other contributors 38 | * Released under the MIT license 39 | * https://jquery.org/license 40 | * 41 | * Date: 2022-08-26T17:52Z 42 | */ 43 | 44 | /*! 45 | * php-unserialize-js JavaScript Library 46 | * https://github.com/bd808/php-unserialize-js 47 | * 48 | * Copyright 2013 Bryan Davis and contributors 49 | * Released under the MIT license 50 | * http://www.opensource.org/licenses/MIT 51 | */ 52 | 53 | /*! svg.draggable.js - v2.2.2 - 2019-01-08 54 | * https://github.com/svgdotjs/svg.draggable.js 55 | * Copyright (c) 2019 Wout Fierens; Licensed MIT */ 56 | 57 | /*! svg.filter.js - v2.0.2 - 2016-02-24 58 | * https://github.com/wout/svg.filter.js 59 | * Copyright (c) 2016 Wout Fierens; Licensed MIT */ 60 | 61 | /** 62 | * @license 63 | * Lodash 64 | * Copyright OpenJS Foundation and other contributors 65 | * Released under MIT license 66 | * Based on Underscore.js 1.8.3 67 | * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 68 | */ 69 | 70 | /** 71 | * Prism: Lightweight, robust, elegant syntax highlighting 72 | * 73 | * @license MIT 74 | * @author Lea Verou 75 | * @namespace 76 | * @public 77 | */ 78 | 79 | /**! 80 | * @fileOverview Kickass library to create and place poppers near their reference elements. 81 | * @version 1.16.1 82 | * @license 83 | * Copyright (c) 2016 Federico Zivolo and contributors 84 | * 85 | * Permission is hereby granted, free of charge, to any person obtaining a copy 86 | * of this software and associated documentation files (the "Software"), to deal 87 | * in the Software without restriction, including without limitation the rights 88 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 89 | * copies of the Software, and to permit persons to whom the Software is 90 | * furnished to do so, subject to the following conditions: 91 | * 92 | * The above copyright notice and this permission notice shall be included in all 93 | * copies or substantial portions of the Software. 94 | * 95 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 96 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 97 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 98 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 99 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 100 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 101 | * SOFTWARE. 102 | */ 103 | 104 | //! Copyright (c) JS Foundation and other contributors 105 | 106 | //! github.com/moment/moment-timezone 107 | 108 | //! license : MIT 109 | 110 | //! moment-timezone.js 111 | 112 | //! moment.js 113 | 114 | //! version : 0.5.38 115 | -------------------------------------------------------------------------------- /public/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laravel-workflow/waterline/bb1df3896a10816e12d0c9499b84979695d650f2/public/img/favicon.png -------------------------------------------------------------------------------- /public/img/sprite.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | 400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | 453 | 454 | 455 | 456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 | 465 | 466 | 467 | 468 | 469 | 470 | 471 | 472 | 473 | 474 | 475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 | 535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 | 545 | 546 | 547 | 548 | 549 | 550 | 551 | 552 | 553 | 554 | 555 | 557 | 558 | 559 | 560 | 561 | 562 | 563 | 564 | 565 | 566 | 567 | 568 | 569 | 570 | 571 | 572 | 573 | 574 | 575 | 576 | 577 | 578 | 579 | 580 | 581 | 582 | 583 | 584 | 585 | 586 | 587 | 588 | 589 | 590 | 591 | 592 | 593 | 594 | 595 | 596 | 597 | 598 | 599 | 600 | 601 | 602 | 603 | 604 | 605 | 606 | 607 | 608 | 609 | 610 | 611 | 612 | 613 | 614 | 615 | 616 | 617 | 618 | 619 | 620 | 621 | 622 | 623 | 624 | 625 | 626 | 627 | 628 | 629 | 630 | 631 | 632 | 633 | 634 | 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | 645 | 646 | 647 | 648 | 649 | 650 | 651 | 652 | 653 | 654 | 655 | 656 | 657 | 658 | 659 | 660 | 661 | 662 | 663 | 664 | 665 | 666 | 667 | 668 | 669 | 670 | 671 | 672 | 673 | 674 | 675 | 676 | 677 | 678 | 679 | 680 | 681 | 682 | 683 | 684 | 685 | 686 | 687 | 688 | 689 | 690 | 691 | 692 | 693 | 694 | 695 | 696 | 697 | 698 | 699 | 700 | 701 | 702 | 703 | 704 | 705 | 706 | 707 | 708 | 709 | 710 | 711 | 712 | 713 | 714 | 715 | 716 | 717 | 718 | 719 | 720 | 721 | 722 | 723 | 724 | 725 | 726 | 727 | 728 | 729 | 730 | 731 | 732 | 733 | 734 | 735 | 736 | 737 | 738 | 739 | 740 | 741 | 742 | 743 | 744 | 745 | 746 | 747 | 748 | 749 | 750 | 751 | 752 | 753 | 754 | 755 | 756 | 757 | 758 | 759 | 760 | 761 | 762 | 763 | 764 | 765 | 766 | 767 | 768 | 769 | 770 | 771 | 772 | 773 | 774 | 775 | 776 | 777 | 778 | 779 | 780 | 781 | 782 | 783 | 784 | 785 | 786 | 787 | 788 | 789 | 790 | 791 | 792 | 793 | 794 | 795 | 796 | 797 | 798 | 799 | 801 | 802 | 803 | 805 | 806 | -------------------------------------------------------------------------------- /public/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/app.js": "/app.js?id=aa6bac08671f4202c964f38c2b7f5cbe", 3 | "/app-dark.css": "/app-dark.css?id=5292caa6c40b30162e4cc7d8be02a626", 4 | "/app.css": "/app.css?id=7c30e7ca2411a0c99b6fd9647cedf354", 5 | "/img/favicon.png": "/img/favicon.png?id=7c006241b093796d6abfa3049df93a59", 6 | "/img/sprite.svg": "/img/sprite.svg?id=afc4952b74895bdef3ab4ebe9adb746f" 7 | } 8 | -------------------------------------------------------------------------------- /resources/img/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/laravel-workflow/waterline/bb1df3896a10816e12d0c9499b84979695d650f2/resources/img/favicon.png -------------------------------------------------------------------------------- /resources/js/app.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Base from './base'; 3 | import axios from 'axios'; 4 | import Routes from './routes'; 5 | import VueRouter from 'vue-router'; 6 | import VueJsonPretty from 'vue-json-pretty'; 7 | import VueApexCharts from 'vue-apexcharts'; 8 | import { PrismEditor } from 'vue-prism-editor'; 9 | 10 | import 'vue-prism-editor/dist/prismeditor.min.css'; 11 | 12 | window.Popper = require('popper.js').default; 13 | 14 | try { 15 | window.$ = window.jQuery = require('jquery'); 16 | 17 | require('bootstrap'); 18 | } catch (e) {} 19 | 20 | let token = document.head.querySelector('meta[name="csrf-token"]'); 21 | 22 | axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; 23 | 24 | if (token) { 25 | axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content; 26 | } 27 | 28 | Vue.use(VueRouter); 29 | 30 | Vue.prototype.$http = axios.create(); 31 | 32 | window.Waterline.basePath = '/' + window.Waterline.path; 33 | 34 | let routerBasePath = window.Waterline.basePath + '/'; 35 | 36 | if (window.Waterline.path === '' || window.Waterline.path === '/') { 37 | routerBasePath = '/'; 38 | window.Waterline.basePath = ''; 39 | } 40 | 41 | const router = new VueRouter({ 42 | routes: Routes, 43 | mode: 'history', 44 | base: routerBasePath, 45 | }); 46 | 47 | Vue.use(VueApexCharts) 48 | 49 | Vue.component('apexchart', VueApexCharts) 50 | Vue.component('vue-json-pretty', VueJsonPretty); 51 | Vue.component('PrismEditor', PrismEditor); 52 | 53 | Vue.mixin(Base); 54 | 55 | Vue.directive('tooltip', function (el, binding) { 56 | $(el).tooltip({ 57 | title: binding.value, 58 | placement: binding.arg, 59 | trigger: 'hover', 60 | }); 61 | }); 62 | 63 | new Vue({ 64 | el: '#waterline', 65 | 66 | router, 67 | 68 | data() { 69 | return { 70 | alert: { 71 | type: null, 72 | autoClose: 0, 73 | message: '', 74 | confirmationProceed: null, 75 | confirmationCancel: null, 76 | }, 77 | 78 | autoLoadsNewEntries: localStorage.autoLoadsNewEntries === '1', 79 | }; 80 | }, 81 | }); 82 | -------------------------------------------------------------------------------- /resources/js/base.js: -------------------------------------------------------------------------------- 1 | import moment from 'moment-timezone'; 2 | 3 | export default { 4 | computed: { 5 | Waterline() { 6 | return Waterline; 7 | }, 8 | }, 9 | 10 | methods: { 11 | /** 12 | * Format the given date with respect to timezone. 13 | */ 14 | formatDate(unixTime) { 15 | return moment(unixTime * 1000).add(new Date().getTimezoneOffset() / 60); 16 | }, 17 | 18 | /** 19 | * Format the given date with respect to timezone. 20 | */ 21 | formatDateIso(date) { 22 | return moment(date).add(new Date().getTimezoneOffset() / 60); 23 | }, 24 | 25 | /** 26 | * Extract the flow base name. 27 | */ 28 | flowBaseName(name) { 29 | if (!name.includes('\\')) return name; 30 | 31 | var parts = name.split('\\'); 32 | 33 | return parts[parts.length - 1]; 34 | }, 35 | 36 | /** 37 | * Autoload new entries in listing screens. 38 | */ 39 | autoLoadNewEntries() { 40 | if (!this.autoLoadsNewEntries) { 41 | this.autoLoadsNewEntries = true; 42 | localStorage.autoLoadsNewEntries = 1; 43 | } else { 44 | this.autoLoadsNewEntries = false; 45 | localStorage.autoLoadsNewEntries = 0; 46 | } 47 | }, 48 | 49 | /** 50 | * Convert to human readable timestamp. 51 | */ 52 | readableTimestamp(timestamp) { 53 | return this.formatDate(timestamp).format('YYYY-MM-DD HH:mm:ss'); 54 | }, 55 | 56 | /** 57 | * Convert to timestamp. 58 | */ 59 | timestamp(timestamp) { 60 | return timestamp.replace('T', ' ').replace('Z', ''); 61 | }, 62 | }, 63 | }; 64 | -------------------------------------------------------------------------------- /resources/js/routes.js: -------------------------------------------------------------------------------- 1 | export default [ 2 | { path: '/', redirect: '/dashboard' }, 3 | 4 | { 5 | path: '/dashboard', 6 | name: 'dashboard', 7 | component: require('./screens/dashboard').default, 8 | }, 9 | 10 | { 11 | path: '/running/:flowId', 12 | name: 'running-flows-preview', 13 | component: require('./screens/flows/flow').default, 14 | }, 15 | 16 | { 17 | path: '/completed/:flowId', 18 | name: 'completed-flows-preview', 19 | component: require('./screens/flows/flow').default, 20 | }, 21 | 22 | { 23 | path: '/failed/:flowId', 24 | name: 'failed-flows-preview', 25 | component: require('./screens/flows/flow').default, 26 | }, 27 | 28 | { 29 | path: '/:type', 30 | name: 'flows', 31 | component: require('./screens/flows/index').default, 32 | }, 33 | ]; 34 | -------------------------------------------------------------------------------- /resources/js/screens/dashboard.vue: -------------------------------------------------------------------------------- 1 | 76 | 77 | 186 | -------------------------------------------------------------------------------- /resources/js/screens/flows/flow-row.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 59 | -------------------------------------------------------------------------------- /resources/js/screens/flows/flow.vue: -------------------------------------------------------------------------------- 1 | 180 | 181 | 374 | -------------------------------------------------------------------------------- /resources/js/screens/flows/index.vue: -------------------------------------------------------------------------------- 1 | 140 | 141 | 201 | -------------------------------------------------------------------------------- /resources/sass/app-dark.scss: -------------------------------------------------------------------------------- 1 | $font-family-base: Nunito, sans-serif; 2 | $font-size-base: 0.95rem; 3 | $badge-font-size: 0.95rem; 4 | 5 | $primary: #adadff; 6 | $secondary: #494444; 7 | $success: #1f9d55; 8 | $info: #1c3d5a; 9 | $warning: #b08d2f; 10 | $danger: #aa2e28; 11 | $gray-800: $secondary; 12 | 13 | $body-bg: #1c1c1c; 14 | $body-color: #e2edf4; 15 | 16 | $sidebar-nav-color: #6e6b6b; 17 | $sidebar-icon-color: #9f9898; 18 | 19 | $pill-link-active: $primary; 20 | 21 | $border-color: #303030; 22 | $table-border-color: #343434; 23 | $table-headers-color: #181818; 24 | $table-hover-bg: #343434; 25 | 26 | $header-border-color: $table-border-color; 27 | 28 | $input-bg: #242424; 29 | $input-color: #e2edf4; 30 | $input-border-color: $table-border-color; 31 | 32 | $card-cap-bg: #120f12; 33 | $card-bg-secondary: #262525; 34 | $card-bg: $card-cap-bg; 35 | $card-shadow-color: $body-bg; 36 | 37 | $code-bg: $card-bg-secondary; 38 | 39 | $paginator-button-color: #9ea7ac; 40 | 41 | $modal-content-bg: $table-headers-color; 42 | $modal-backdrop-bg: #7e7e7e; 43 | $modal-footer-border-color: $input-border-color; 44 | $modal-header-border-color: $input-border-color; 45 | 46 | $new-entries-bg: #505e4a; 47 | 48 | $control-action-icon-color: #ccd2df; 49 | 50 | $dropdown-bg: $table-headers-color; 51 | $dropdown-link-color: #fff; 52 | 53 | $grid-breakpoints: ( 54 | xs: 0, 55 | sm: 2px, 56 | md: 8px, 57 | lg: 9px, 58 | xl: 10px 59 | ) !default; 60 | 61 | $container-max-widths: ( 62 | sm: 1137px, 63 | md: 1138px, 64 | lg: 1139px, 65 | xl: 1140px 66 | ) !default; 67 | 68 | @import 'base'; 69 | -------------------------------------------------------------------------------- /resources/sass/app.scss: -------------------------------------------------------------------------------- 1 | $font-family-base: Nunito, sans-serif; 2 | 3 | $font-size-base: 0.95rem; 4 | $badge-font-size: 0.95rem; 5 | 6 | $primary: #7746ec; 7 | $secondary: #dae1e7; 8 | $success: #51d88a; 9 | $info: #bcdefa; 10 | $warning: #ffa260; 11 | $danger: #ef5753; 12 | 13 | $body-bg: #ebebeb; 14 | 15 | $btn-focus-width: 0; 16 | 17 | $sidebar-nav-color: #2a5164; 18 | $sidebar-icon-color: #c3cbd3; 19 | 20 | $pill-link-active: $primary; 21 | 22 | $border-color: #efefef; 23 | $table-headers-color: #f3f4f6; 24 | $table-border-color: #efefef; 25 | $table-hover-bg: #f1f7fa; 26 | 27 | $header-border-color: #d5dfe9; 28 | 29 | $card-cap-bg: #fff; 30 | $card-bg-secondary: #fafafa; 31 | $card-shadow-color: #cdd8df; 32 | 33 | $code-bg: #120f12; 34 | 35 | $paginator-button-color: #9ea7ac; 36 | 37 | $new-entries-bg: #fffee9; 38 | 39 | $control-action-icon-color: #ccd2df; 40 | 41 | $grid-breakpoints: ( 42 | xs: 0, 43 | sm: 2px, 44 | md: 8px, 45 | lg: 9px, 46 | xl: 10px 47 | ) !default; 48 | 49 | $container-max-widths: ( 50 | sm: 1137px, 51 | md: 1138px, 52 | lg: 1139px, 53 | xl: 1140px 54 | ) !default; 55 | 56 | @import 'base'; 57 | -------------------------------------------------------------------------------- /resources/sass/base.scss: -------------------------------------------------------------------------------- 1 | @import 'syntaxhighlight'; 2 | @import 'node_modules/bootstrap/scss/bootstrap'; 3 | 4 | body { 5 | padding-bottom: 20px; 6 | } 7 | 8 | .container { 9 | width: 1140px; 10 | } 11 | 12 | html { 13 | min-width: 1140px; 14 | } 15 | 16 | [v-cloak] { 17 | display: none; 18 | } 19 | 20 | svg.icon { 21 | width: 1rem; 22 | height: 1rem; 23 | } 24 | 25 | .header { 26 | border-bottom: solid 1px $header-border-color; 27 | 28 | svg.logo { 29 | width: 2rem; 30 | height: 2rem; 31 | transform: rotate(180deg); 32 | } 33 | } 34 | 35 | .sidebar .nav-item { 36 | a { 37 | color: $sidebar-nav-color; 38 | padding: 0.5rem 0rem; 39 | 40 | svg { 41 | width: 1rem; 42 | height: 1rem; 43 | margin-right: 15px; 44 | fill: $sidebar-icon-color; 45 | } 46 | 47 | &.active { 48 | color: $primary; 49 | 50 | svg { 51 | fill: $primary; 52 | } 53 | } 54 | } 55 | } 56 | 57 | .card { 58 | box-shadow: 0 2px 3px $card-shadow-color; 59 | border: none; 60 | 61 | .bottom-radius { 62 | border-bottom-left-radius: $card-border-radius; 63 | border-bottom-right-radius: $card-border-radius; 64 | } 65 | 66 | .card-header { 67 | padding-top: 0.7rem; 68 | padding-bottom: 0.7rem; 69 | background-color: $card-cap-bg; 70 | border-bottom: none; 71 | 72 | .btn-group { 73 | .btn { 74 | padding: 0.2rem 0.5rem; 75 | } 76 | } 77 | 78 | h5 { 79 | margin: 0; 80 | } 81 | } 82 | 83 | .table { 84 | th, 85 | td { 86 | padding: 0.75rem 1.25rem; 87 | } 88 | 89 | &.table-sm { 90 | th, 91 | td { 92 | padding: 1rem 1.25rem; 93 | } 94 | } 95 | 96 | th { 97 | background-color: $table-headers-color; 98 | font-weight: 400; 99 | padding: 0.5rem 1.25rem; 100 | border-bottom: 0; 101 | } 102 | 103 | &:not(.table-borderless) { 104 | td { 105 | border-top: 1px solid $table-border-color; 106 | } 107 | } 108 | 109 | &.penultimate-column-right { 110 | th:nth-last-child(2), 111 | td:nth-last-child(2) { 112 | text-align: right; 113 | } 114 | } 115 | 116 | th.table-fit, 117 | td.table-fit { 118 | width: 1%; 119 | white-space: nowrap; 120 | } 121 | } 122 | } 123 | 124 | .fill-text-color { 125 | fill: $body-color; 126 | } 127 | 128 | .fill-danger { 129 | fill: $danger; 130 | } 131 | 132 | .fill-warning { 133 | fill: $warning; 134 | } 135 | 136 | .fill-info { 137 | fill: $info; 138 | } 139 | 140 | .fill-success { 141 | fill: $success; 142 | } 143 | 144 | .fill-primary { 145 | fill: $primary; 146 | } 147 | 148 | button:hover { 149 | .fill-primary { 150 | fill: #fff; 151 | } 152 | } 153 | 154 | .btn-outline-primary.active { 155 | .fill-primary { 156 | fill: $body-bg; 157 | } 158 | } 159 | 160 | .btn-outline-primary:not(:disabled):not(.disabled).active:focus { 161 | box-shadow: none !important; 162 | } 163 | 164 | .control-action { 165 | svg { 166 | fill: $control-action-icon-color; 167 | width: 1.2rem; 168 | height: 1.2rem; 169 | 170 | &:hover { 171 | fill: $primary; 172 | } 173 | } 174 | } 175 | 176 | .info-icon { 177 | fill: $control-action-icon-color; 178 | } 179 | 180 | .paginator { 181 | .btn { 182 | text-decoration: none; 183 | color: $paginator-button-color; 184 | 185 | &:hover { 186 | color: $primary; 187 | } 188 | } 189 | } 190 | 191 | @-webkit-keyframes spin { 192 | from { 193 | -ms-transform: rotate(0deg); 194 | -moz-transform: rotate(0deg); 195 | -webkit-transform: rotate(0deg); 196 | -o-transform: rotate(0deg); 197 | transform: rotate(0deg); 198 | } 199 | to { 200 | -ms-transform: rotate(360deg); 201 | -moz-transform: rotate(360deg); 202 | -webkit-transform: rotate(360deg); 203 | -o-transform: rotate(360deg); 204 | transform: rotate(360deg); 205 | } 206 | } 207 | 208 | @keyframes spin { 209 | from { 210 | -ms-transform: rotate(0deg); 211 | -moz-transform: rotate(0deg); 212 | -webkit-transform: rotate(0deg); 213 | -o-transform: rotate(0deg); 214 | transform: rotate(0deg); 215 | } 216 | to { 217 | -ms-transform: rotate(360deg); 218 | -moz-transform: rotate(360deg); 219 | -webkit-transform: rotate(360deg); 220 | -o-transform: rotate(360deg); 221 | transform: rotate(360deg); 222 | } 223 | } 224 | 225 | .spin { 226 | -webkit-animation: spin 2s linear infinite; 227 | -moz-animation: spin 2s linear infinite; 228 | -ms-animation: spin 2s linear infinite; 229 | -o-animation: spin 2s linear infinite; 230 | animation: spin 2s linear infinite; 231 | } 232 | 233 | .card { 234 | .nav-pills .nav-link.active { 235 | background: none; 236 | color: $pill-link-active; 237 | border-bottom: solid 2px $primary; 238 | } 239 | 240 | .nav-pills .nav-link { 241 | font-size: 0.9rem; 242 | border-radius: 0; 243 | padding: 0.75rem 1.25rem; 244 | color: $body-color; 245 | } 246 | } 247 | 248 | .list-enter-active:not(.dontanimate) { 249 | transition: background 1s linear; 250 | } 251 | 252 | .list-enter:not(.dontanimate), 253 | .list-leave-to:not(.dontanimate) { 254 | background: $new-entries-bg; 255 | } 256 | 257 | .card table { 258 | td { 259 | vertical-align: middle !important; 260 | } 261 | } 262 | 263 | .card-bg-secondary { 264 | background: $card-bg-secondary; 265 | } 266 | 267 | .code-bg { 268 | background: $code-bg; 269 | } 270 | 271 | .disabled-watcher { 272 | padding: 0.75rem; 273 | color: #fff; 274 | background: $danger; 275 | } 276 | 277 | .badge-sm { 278 | font-size: 0.75rem; 279 | } 280 | -------------------------------------------------------------------------------- /resources/sass/syntaxhighlight.scss: -------------------------------------------------------------------------------- 1 | .vjs-tree { 2 | font-family: 'Monaco', 'Menlo', 'Consolas', 'Bitstream Vera Sans Mono', monospace !important; 3 | 4 | &.is-root { 5 | position: relative; 6 | } 7 | .vjs-tree__content { 8 | padding-left: 1em; 9 | &.has-line { 10 | border-left: 1px dotted rgba(204, 204, 204, 0.28) !important; 11 | } 12 | } 13 | .vjs-tree__brackets { 14 | cursor: pointer; 15 | &:hover { 16 | color: #20a0ff; 17 | } 18 | } 19 | .vjs-value__null, 20 | .vjs-value__number, 21 | .vjs-value__boolean { 22 | color: #a291f5 !important; 23 | } 24 | .vjs-value__string { 25 | color: #dacb4d !important; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /resources/views/layout.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Waterline{{ config('app.name') ? ' - ' . config('app.name') : '' }} 11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 24 | 25 |
26 |
27 | 30 | 31 |

32 | Waterline{{ config('app.name') ? ' - ' . config('app.name') : '' }}

33 | 34 | 39 |
40 | 41 |
42 | 78 | 79 |
80 | @if (! $assetsAreCurrent) 81 |
82 | The published Waterline assets are not up-to-date with the installed version. To update, run:
php artisan waterline:publish 83 |
84 | @endif 85 | 86 | @if ($isDownForMaintenance) 87 |
88 | This application is in "maintenance mode". Queued jobs may not be processed unless your worker is using the "force" flag. 89 |
90 | @endif 91 | 92 | 93 |
94 |
95 |
96 |
97 | 98 | 99 | 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | group(function () { 17 | Route::get('/stats', 'DashboardStatsController@index')->name('waterline.stats.index'); 18 | 19 | Route::get('/flows/completed', 'WorkflowsController@completed')->name('waterline.completed'); 20 | Route::get('/flows/failed', 'WorkflowsController@failed')->name('waterline.failed'); 21 | Route::get('/flows/running', 'WorkflowsController@running')->name('waterline.running'); 22 | Route::get('/flows/{id}', 'WorkflowsController@show')->name('waterline.show'); 23 | }); 24 | 25 | Route::get('/{view?}', 'DashboardController@index')->where('view', '(.*)')->name('waterline.index'); 26 | -------------------------------------------------------------------------------- /tests/Feature/DashboardStatsControllerTest.php: -------------------------------------------------------------------------------- 1 | get('/waterline/api/stats'); 14 | 15 | $response 16 | ->assertStatus(200) 17 | ->assertJson([ 18 | 'flows' => 0, 19 | 'flows_per_minute' => 0, 20 | 'flows_past_hour' => 0, 21 | 'exceptions_past_hour' => 0, 22 | 'failed_flows_past_week' => 0, 23 | 'max_wait_time_workflow' => null, 24 | 'max_duration_workflow' => null, 25 | 'max_exceptions_workflow' => null, 26 | ]); 27 | } 28 | 29 | public function testIndexOne() 30 | { 31 | $workflow = StoredWorkflow::create([ 32 | 'class' => 'class', 33 | 'arguments' => null, 34 | 'output' => null, 35 | 'status' => 'created', 36 | ]); 37 | 38 | $response = $this 39 | ->get('/waterline/api/stats'); 40 | 41 | $response 42 | ->assertStatus(200) 43 | ->assertJson([ 44 | 'flows' => 1, 45 | 'flows_per_minute' => 0.016666666666666666, 46 | 'flows_past_hour' => 1, 47 | 'exceptions_past_hour' => 0, 48 | 'failed_flows_past_week' => 0, 49 | 'max_wait_time_workflow' => null, 50 | 'max_duration_workflow' => $workflow->toArray(), 51 | 'max_exceptions_workflow' => null, 52 | ]); 53 | } 54 | 55 | public function testIndexTwo() 56 | { 57 | $workflows = [StoredWorkflow::create([ 58 | 'class' => 'class', 59 | 'arguments' => null, 60 | 'output' => null, 61 | 'status' => 'created', 62 | ]), StoredWorkflow::create([ 63 | 'class' => 'class', 64 | 'arguments' => null, 65 | 'output' => null, 66 | 'status' => 'created', 67 | ])]; 68 | 69 | $response = $this 70 | ->get('/waterline/api/stats'); 71 | 72 | $response 73 | ->assertStatus(200) 74 | ->assertJson([ 75 | 'flows' => 2, 76 | 'flows_per_minute' => 0.03333333333333333, 77 | 'flows_past_hour' => 2, 78 | 'exceptions_past_hour' => 0, 79 | 'failed_flows_past_week' => 0, 80 | 'max_wait_time_workflow' => null, 81 | 'max_duration_workflow' => $workflows[0]->toArray(), 82 | 'max_exceptions_workflow' => null, 83 | ]); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tests/Feature/DashboardWorkflowTest.php: -------------------------------------------------------------------------------- 1 | 'WorkflowClass', 17 | 'arguments' => 'N;', 18 | 'output' => 'N;', 19 | 'status' => 'created', 20 | ]); 21 | 22 | $storedLog = $storedWorkflow->logs()->create([ 23 | 'index' => 0, 24 | 'now' => now()->toDateTimeString(), 25 | 'class' => 'Activity1Class', 26 | 'result' => 'N;', 27 | ]); 28 | 29 | $storedWorkflow->exceptions()->create([ 30 | 'class' => 'Activity2Class', 31 | 'exception' => Serializer::serialize(new Exception('ExceptionMessage')), 32 | ]); 33 | 34 | 35 | $response = $this 36 | ->get('/waterline/api/flows/'.$storedWorkflow->id); 37 | 38 | $response 39 | ->assertStatus(200) 40 | ->assertJson( 41 | fn (AssertableJson $json) => $json 42 | ->where('id', $storedWorkflow->id) 43 | ->where('class', 'WorkflowClass') 44 | ->where('arguments', 'N;') 45 | ->where('output', 'N;') 46 | ->where('status', 'created') 47 | ->whereType('created_at', 'string') 48 | ->whereType('updated_at', 'string') 49 | ->has( 50 | 'logs', 51 | 1, 52 | fn (AssertableJson $log) => $log 53 | ->where('id', $storedLog->id) 54 | ->where('index', 0) 55 | ->whereType('now', 'string') 56 | ->where('class', 'Activity1Class') 57 | ->where('result', 'N;') 58 | ->whereType('created_at', 'string') 59 | ) 60 | ->has( 61 | 'exceptions', 62 | 1, 63 | fn (AssertableJson $exception) => $exception 64 | ->where( 65 | 'id', 66 | fn ($value) => is_string($value) || is_int($value) 67 | ) 68 | ->whereType('code', 'string') 69 | ->whereType('exception', 'string') 70 | ->where('class', 'Activity2Class') 71 | ->whereType('created_at', 'string') 72 | ) 73 | ->has('chartData', 2) 74 | ->where('chartData.0.x', 'WorkflowClass') 75 | ->where('chartData.0.type', 'Workflow') 76 | ->where('chartData.1.x', 'Activity1Class') 77 | ->where('chartData.1.type', 'Activity') 78 | ->whereAllType([ 79 | 'chartData.0.y.0' => 'integer', 80 | 'chartData.0.y.1' => 'integer', 81 | 'chartData.1.y.0' => 'integer', 82 | 'chartData.1.y.1' => 'integer', 83 | ]) 84 | 85 | ); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | set('app.debug', true); 28 | $app['config']->set('app.key', 'base64:UTyp33UhGolgzCK5CJmT+hNHcA+dJyp3+oINtX+VoPI='); 29 | } 30 | 31 | protected function defineDatabaseMigrations() 32 | { 33 | $this->app->bind('db.connector.sqlsrv', function () { 34 | return new class extends \Illuminate\Database\Connectors\SqlServerConnector 35 | { 36 | protected $options = [ 37 | PDO::ATTR_CASE => PDO::CASE_NATURAL, 38 | PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, 39 | PDO::ATTR_ORACLE_NULLS => PDO::NULL_NATURAL, 40 | ]; 41 | }; 42 | }); 43 | 44 | artisan($this, 'migrate:fresh'); 45 | $this->loadLaravelMigrations(); 46 | $this->loadMigrationsFrom('./vendor/laravel-workflow/laravel-workflow/src/migrations'); 47 | 48 | $this->beforeApplicationDestroyed( 49 | fn () => artisan($this, 'migrate:rollback') 50 | ); 51 | } 52 | 53 | protected function getPackageProviders($app) 54 | { 55 | if (! class_exists('\Workflow\Models\Model')) { 56 | if (env('DB_CONNECTION') === 'mongodb') { 57 | class_alias(\Jenssegers\Mongodb\Eloquent\Model::class, '\Workflow\Models\Model'); 58 | } else { 59 | class_alias(\Illuminate\Database\Eloquent\Model::class, '\Workflow\Models\Model'); 60 | } 61 | } 62 | 63 | $app['config']->set('database.connections.mongodb', [ 64 | 'driver' => 'mongodb', 65 | 'host' => env('DB_HOST', '127.0.0.1'), 66 | 'port' => env('DB_PORT', 27017), 67 | 'database' => env('DB_DATABASE', 'homestead'), 68 | 'username' => env('DB_USERNAME', 'homestead'), 69 | 'password' => env('DB_PASSWORD', 'secret'), 70 | ]); 71 | 72 | return ['Jenssegers\Mongodb\MongodbServiceProvider', 'Waterline\WaterlineServiceProvider', 'Waterline\WaterlineApplicationServiceProvider']; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/Unit/ExampleTest.php: -------------------------------------------------------------------------------- 1 | assertTrue(true); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | const mix = require('laravel-mix'); 2 | const webpack = require('webpack'); 3 | const path = require('path'); 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 | .setPublicPath('public') 26 | .js('resources/js/app.js', 'public') 27 | .vue() 28 | .sass('resources/sass/app.scss', 'public') 29 | .sass('resources/sass/app-dark.scss', 'public') 30 | .version() 31 | .copy('resources/img', 'public/img') 32 | .webpackConfig({ 33 | resolve: { 34 | symlinks: false, 35 | alias: { 36 | '@': path.resolve(__dirname, 'resources/js/'), 37 | }, 38 | }, 39 | plugins: [ 40 | new webpack.IgnorePlugin({ 41 | resourceRegExp: /^\.\/locale$/, 42 | contextRegExp: /moment$/, 43 | }), 44 | ], 45 | }); 46 | --------------------------------------------------------------------------------