├── .php_cs.dist.php ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── model-stats.php ├── database ├── factories │ └── DashboardFactory.php └── migrations │ └── create_model-stats_table.php ├── img.png ├── package-lock.json ├── package.json ├── public ├── app.css ├── app.js ├── app.js.LICENSE.txt ├── mix-manifest.json ├── resources_js_views_dashboard_vue.js └── resources_js_views_errors_404_vue.js ├── resources ├── js │ ├── app.js │ ├── base.js │ ├── components │ │ ├── App.vue │ │ ├── Layout.vue │ │ ├── Loading.vue │ │ ├── charts │ │ │ ├── BarChart.vue │ │ │ └── LineChart.vue │ │ ├── common │ │ │ ├── Alert.vue │ │ │ ├── VButton.vue │ │ │ └── index.js │ │ ├── custom-code │ │ │ ├── CodeInput.vue │ │ │ └── CodeOutput.vue │ │ ├── dashboards │ │ │ ├── AddDashboard.vue │ │ │ ├── DashboardDateRange.vue │ │ │ └── EditDashboard.vue │ │ ├── forms │ │ │ ├── CheckboxInput.vue │ │ │ ├── SelectInput.vue │ │ │ ├── TextInput.vue │ │ │ ├── components │ │ │ │ ├── VCheckbox.vue │ │ │ │ └── VSelect.vue │ │ │ ├── index.js │ │ │ ├── model-stats │ │ │ │ ├── DashboardInput.vue │ │ │ │ └── ModelInput.vue │ │ │ └── validation │ │ │ │ ├── Alert.js │ │ │ │ ├── AlertError.vue │ │ │ │ ├── AlertSuccess.vue │ │ │ │ └── HasError.vue │ │ └── widgets │ │ │ ├── AddWidget.vue │ │ │ ├── WidgetList.vue │ │ │ └── components │ │ │ ├── CustomCode.vue │ │ │ ├── DailyCount.vue │ │ │ ├── GroupByCount.vue │ │ │ └── PeriodTotal.vue │ ├── data │ │ └── colors.js │ ├── router │ │ ├── index.js │ │ └── routes.js │ ├── store │ │ ├── index.js │ │ └── modules │ │ │ ├── dashboards.js │ │ │ └── widgets.js │ └── views │ │ ├── dashboard.vue │ │ ├── dashboards │ │ ├── create.vue │ │ └── edit.vue │ │ ├── errors │ │ └── 404.vue │ │ └── widgets │ │ └── create.vue ├── sass │ └── app.scss └── views │ ├── .gitkeep │ └── dashboard.blade.php ├── routes └── web.php ├── src ├── AuthorizesRequests.php ├── Console │ ├── InstallModelStatsPackage.php │ └── PublishCommand.php ├── Http │ ├── Controllers │ │ ├── Controller.php │ │ ├── CustomCodeController.php │ │ ├── DashboardController.php │ │ ├── HomeController.php │ │ └── StatController.php │ ├── Middleware │ │ ├── Authorize.php │ │ └── CustomCodeEnabled.php │ └── Requests │ │ ├── Dashboard │ │ ├── StoreRequest.php │ │ └── UpdateRequest.php │ │ └── Widgets │ │ └── DataRequest.php ├── LaravelModelStats.php ├── LaravelModelStatsFacade.php ├── LaravelModelStatsServiceProvider.php ├── ModelStatsServiceProvider.php ├── Models │ └── Dashboard.php └── Services │ ├── ModelStats.php │ └── Tinker.php ├── stubs └── ModelStatsServiceProvider.stub ├── tailwind.config.js └── webpack.mix.js /.php_cs.dist.php: -------------------------------------------------------------------------------- 1 | in([ 5 | __DIR__ . '/src', 6 | __DIR__ . '/tests', 7 | ]) 8 | ->name('*.php') 9 | ->notName('*.blade.php') 10 | ->ignoreDotFiles(true) 11 | ->ignoreVCS(true); 12 | 13 | return (new PhpCsFixer\Config()) 14 | ->setRules([ 15 | '@PSR2' => true, 16 | 'array_syntax' => ['syntax' => 'short'], 17 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 18 | 'no_unused_imports' => true, 19 | 'not_operator_with_successor_space' => true, 20 | 'trailing_comma_in_multiline' => true, 21 | 'phpdoc_scalar' => true, 22 | 'unary_operator_spaces' => true, 23 | 'binary_operator_spaces' => true, 24 | 'blank_line_before_statement' => [ 25 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], 26 | ], 27 | 'phpdoc_single_line_var_spacing' => true, 28 | 'phpdoc_var_without_name' => true, 29 | 'class_attributes_separation' => [ 30 | 'elements' => [ 31 | 'method' => 'one', 32 | ], 33 | ], 34 | 'method_argument_space' => [ 35 | 'on_multiline' => 'ensure_fully_multiline', 36 | 'keep_multiple_spaces_after_comma' => true, 37 | ], 38 | 'single_trait_insert_per_statement' => true, 39 | ]) 40 | ->setFinder($finder); 41 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-model-stats` will be documented in this file. 4 | 5 | ## 1.0.0 - 2021-06-22 6 | 7 | - initial release 8 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) JhumanJ 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 | # Laravel Model Stats 2 | Model statistics dashboard for your Laravel Application 3 | 4 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/jhumanj/laravel-model-stats.svg?style=flat-square)](https://packagist.org/packages/jhumanj/laravel-model-stats) 5 | [![GitHub Tests Action Status](https://img.shields.io/github/workflow/status/jhumanj/laravel-model-stats/run-tests?label=tests)](https://github.com/jhumanj/laravel-model-stats/actions?query=workflow%3Arun-tests+branch%3Amain) 6 | [![GitHub Code Style Action Status](https://img.shields.io/github/workflow/status/jhumanj/laravel-model-stats/Check%20&%20fix%20styling?label=code%20style)](https://github.com/jhumanj/laravel-model-stats/actions?query=workflow%3A"Check+%26+fix+styling"+branch%3Amain) 7 | [![Total Downloads](https://img.shields.io/packagist/dt/jhumanj/laravel-model-stats.svg?style=flat-square)](https://packagist.org/packages/jhumanj/laravel-model-stats) 8 | 9 | --- 10 | Screenshot of sample dashboard 11 | 12 | This Laravel packages gives you a statistic dashboard for you Laravel application. Think of it as a light version of 13 | [Grafana](https://grafana.com/), but built-in your Laravel application, and much easier to get started with. 14 | No code knowledge is required to use Laravel Model Stats, users can do everything from the web interface. It also 15 | optionally supports custom-code widgets, allowing you to define your widget data with 16 | code, just like you would do with tinker. 17 | 18 | --- 19 | 20 | ## Installation 21 | 22 | You can install the package via composer: 23 | 24 | ```bash 25 | composer require jhumanj/laravel-model-stats 26 | ``` 27 | 28 | You can install the package and run the migrations with: 29 | 30 | ```bash 31 | php artisan model-stats:install 32 | php artisan migrate 33 | ``` 34 | 35 | 36 | ## Available No-Code Widgets 37 | 38 | Different type of widgets (daily count, daily average, etc.) are available. When creating a widget, 39 | you choose a Model, an aggregation type and the column(s) for the graph. You can then resize and move the widgets around on your dashboard. 40 | 41 | The aggregation types currently available: 42 | - Daily Count (Number of records per day during selected period). 43 | - Cumulated Daily Count (Cumulated Total record count during selected period). 44 | - Period Total (Number of new records during selected period). 45 | - Group By Count (Count per group for a given column during selected period). 46 | - ... (more to come soon) 47 | 48 | For each widget type, date can be any column: `created_at`,`updated_at`,`custom_date`. 49 | 50 | ## Custom Code Widgets 51 | 52 | You can also use custom code widgets, allowing you to define your widget data with 53 | code, just like you would do with tinker. 54 | 55 | Your code must define a `$result` variable containing the data to return to the choosen chart. You can use the `$dateFrom` and `$dateTo` variable. 56 | 57 | Example custom code for a bar chart: 58 | 59 | ```php 60 | $result = [ 61 | 'a' => 10, 62 | 'b' => 20 63 | ]; 64 | ``` 65 | 66 | ### Custom Code Setup 67 | 🚨 Using the custom code feature against a production database is a HUGE risk 🚨 68 | 69 | Any malicious user with access to the dashboard, 70 | or any mistake can cause harm to your database. Do not do that. Here's a safe way to use this feature: 71 | - Create a `read-only` database user with access to your database 72 | - Here's how to create a read-only user for a PostgreSQL database: [PostgreSQL guide](https://tableplus.com/blog/2018/04/postgresql-how-to-create-read-only-user.html) 73 | - Here's how to create a read-only user for a MySQL database: [MySQL guide](https://ubiq.co/database-blog/create-read-only-mysql-user/) 74 | - Add a readonly database connection to your `config/database.php` file 75 | ```php 76 | // in database.php 77 | 78 | 'connections' => [ 79 | 80 | // ... your other connections 81 | 82 | 'readonly' => [ 83 | 'driver' => 'pgsql', // Copy the settings for the driver you use, but change the user 84 | 'url' => env('DATABASE_URL'), 85 | 'host' => env('DB_HOST', '127.0.0.1'), 86 | 'port' => env('DB_PORT', '5432'), 87 | 'database' => env('DB_DATABASE', 'forge'), 88 | 'username' => env('DB_USERNAME_READONLY', 'forge'), // User is changed here 89 | 'password' => env('DB_PASSWORD_READONLY', ''), 90 | 'charset' => 'utf8', 91 | 'prefix' => '', 92 | 'prefix_indexes' => true, 93 | 'schema' => 'public', 94 | 'sslmode' => 'prefer', 95 | ], 96 | ] 97 | - In your .env set the following: 98 | ```dotenv 99 | MODEL_STATS_CUSTOM_CODE=true 100 | MODEL_STATS_DB_CONNECTION=readonly 101 | DB_USERNAME_READONLY= 102 | DB_PASSWORD_READONLY= 103 | ``` 104 | Thanks to this, the package will use the readonly connection when executing your code. 105 | Note that this a protection against mistakes, but not against malicious users. One can override this 106 | connection in the custom code, so there are still some risks associate with using this feature in production. 107 | Be sure that your dashboard authorization is properly configured. 108 | 109 | ### Disabling Custom Code 110 | You may want to disable custom code widgets by setting the `MODEL_STATS_CUSTOM_CODE` env variable to `false`. 111 | 112 | ## Dashboard Authorization 113 | 114 | The ModelStats dashboard may be accessed at the `/stats` route. By default, you will only be able to access this 115 | dashboard in the local environment. Within your `app/Providers/ModelStatsServiceProvider.php` file, there is an 116 | authorization gate definition. This authorization gate controls access to ModelStats in non-local environments. 117 | You are free to modify this gate as needed to restrict access to your ModelStats installation: 118 | 119 | ```php 120 | /** 121 | * Register the ModelStats gate. 122 | * 123 | * This gate determines who can access ModelStats in non-local environments. 124 | * 125 | * @return void 126 | */ 127 | protected function gate() 128 | { 129 | Gate::define('viewModelStats', function ($user) { 130 | return in_array($user->email, [ 131 | 'taylor@laravel.com', 132 | ]); 133 | }); 134 | } 135 | ``` 136 | 137 | ## Upgrading 138 | 139 | Be sure to re-publish the front-end assets when upgrading ModelStats: 140 | ``` 141 | php artisan model-stats:publish 142 | ``` 143 | 144 | ## Changelog 145 | 146 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 147 | 148 | ## Contributing 149 | 150 | Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. 151 | 152 | ## Security Vulnerabilities 153 | 154 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 155 | 156 | ## Credits (Contributors) 157 | 158 | - [Julien Nahum](https://github.com/JhumanJ) 159 | 160 | ## Inspiration 161 | - [Grafana](https://grafana.com/): for the dashboard/widget aspect 162 | - [Laravel/Telescope](https://github.com/laravel/telescope): for many things in the package structure (front-end, authorization...) 163 | - [Spatie/Laravel-Web-Tinker](https://github.com/spatie/laravel-web-tinker): for their web tinker implementation, which is used for custom code widgets 164 | 165 | ## License 166 | 167 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 168 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jhumanj/laravel-model-stats", 3 | "description": "Model statistics dashboard for your Laravel Application", 4 | "keywords": [ 5 | "JhumanJ", 6 | "laravel", 7 | "laravel-model-stats" 8 | ], 9 | "homepage": "https://github.com/jhumanj/laravel-model-stats", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Julien Nahum", 14 | "email": "julien@nahum.net", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.0|^8.1|^8.2", 20 | "spatie/laravel-package-tools": "^1.4", 21 | "illuminate/contracts": "^8.0|^v9.0|^v10.0", 22 | "illuminate/support": "^8.0|^9.0|^10.0", 23 | "laravel/tinker": "^1.0|^2.0" 24 | }, 25 | "require-dev": { 26 | "brianium/paratest": "^6.2|^7.0", 27 | "nunomaduro/collision": "^5.3|^6.0|^7.0", 28 | "orchestra/testbench": "^6.15|^7.0|^8.0", 29 | "phpunit/phpunit": "^9.3|^10.0", 30 | "spatie/laravel-ray": "^1.2", 31 | "vimeo/psalm": "^4.4|^5.0" 32 | }, 33 | "autoload": { 34 | "psr-4": { 35 | "Jhumanj\\LaravelModelStats\\": "src", 36 | "Jhumanj\\LaravelModelStats\\Database\\Factories\\": "database/factories" 37 | } 38 | }, 39 | "autoload-dev": { 40 | "psr-4": { 41 | "Jhumanj\\LaravelModelStats\\Tests\\": "tests" 42 | } 43 | }, 44 | "scripts": { 45 | "psalm": "vendor/bin/psalm", 46 | "test": "./vendor/bin/testbench package:test --parallel --no-coverage", 47 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage" 48 | }, 49 | "config": { 50 | "sort-packages": true 51 | }, 52 | "extra": { 53 | "laravel": { 54 | "providers": [ 55 | "Jhumanj\\LaravelModelStats\\LaravelModelStatsServiceProvider" 56 | ] 57 | } 58 | }, 59 | "minimum-stability": "dev", 60 | "prefer-stable": true 61 | } 62 | -------------------------------------------------------------------------------- /config/model-stats.php: -------------------------------------------------------------------------------- 1 | env('MODEL_STATS_ENABLED', true), 20 | 'allow_custom_code' => env('MODEL_STATS_CUSTOM_CODE', false), 21 | 22 | /* 23 | |-------------------------------------------------------------------------- 24 | | Route Middleware 25 | |-------------------------------------------------------------------------- 26 | | 27 | | These middleware will be assigned to every ModelStats route, giving you 28 | | the chance to add your own middleware to this list or change any of 29 | | the existing middleware. Or, you can simply stick with this list. 30 | | 31 | */ 32 | 'middleware' => [ 33 | 'web', 34 | \Jhumanj\LaravelModelStats\Http\Middleware\Authorize::class, 35 | ], 36 | 37 | /* 38 | |-------------------------------------------------------------------------- 39 | | ModelStats table name 40 | |-------------------------------------------------------------------------- 41 | | 42 | | As PostgreSQL table names seems to use dashes instead of underscore 43 | | this configures the table name based on your connection. 44 | | 45 | */ 46 | 'table_name' => 'model_stats_dashboards', 47 | 48 | /* 49 | |-------------------------------------------------------------------------- 50 | | Database connection 51 | |-------------------------------------------------------------------------- 52 | | 53 | | Database connection used to query the data. 54 | | This can be used to ensure a read-only connection, by using a custom connection with a read-only user. 55 | | 56 | */ 57 | 'query_database_connection' => env('MODEL_STATS_DB_CONNECTION', env('DB_CONNECTION')), 58 | 59 | /* 60 | |-------------------------------------------------------------------------- 61 | | Route Prefixes 62 | |-------------------------------------------------------------------------- 63 | | 64 | | You can change the route where your dashboards are. By default, routes will 65 | | be starting the '/stats' prefix, and names will start with 'stats.'. 66 | | 67 | */ 68 | 'routes_prefix' => 'stats', 69 | 'route_names_prefix' => 'stats.', 70 | ]; 71 | -------------------------------------------------------------------------------- /database/factories/DashboardFactory.php: -------------------------------------------------------------------------------- 1 | $this->faker->name, 16 | 'description' => $this->faker->sentence, 17 | 'body' => '{"widgets":[]}', 18 | ]; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /database/migrations/create_model-stats_table.php: -------------------------------------------------------------------------------- 1 | id(); 14 | $table->string('name'); 15 | $table->text('description'); 16 | if (Config::get('database.defaults') === 'pgsql') { 17 | $table->jsonb('body')->default('{"widgets":[]}'); 18 | } else { 19 | $table->json('body'); 20 | } 21 | $table->timestamps(); 22 | }); 23 | } 24 | 25 | public function down(): void 26 | { 27 | Schema::dropIfExists(Config::get('model-stats.table_name')); 28 | } 29 | }; 30 | -------------------------------------------------------------------------------- /img.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/JhumanJ/laravel-model-stats/88a47ed2a9dd784f2e4d70f30cf103d50bac5d0b/img.png -------------------------------------------------------------------------------- /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 | "axios": "^0.21", 14 | "laravel-mix": "^6.0.6", 15 | "lodash": "^4.17.19", 16 | "moment": "^2.10.6", 17 | "moment-timezone": "^0.5.21", 18 | "resolve-url-loader": "^3.1.2", 19 | "sass": "^1.15.2", 20 | "sass-loader": "^11.0.1", 21 | "tailwindcss": "^2.1.1", 22 | "vform": "^1.0.1", 23 | "vue": "^2.5.7", 24 | "vue-clickaway": "^2.2.2", 25 | "vue-loader": "^15.9.6", 26 | "vue-router": "^3.0.1", 27 | "vue-template-compiler": "^2.5.21", 28 | "vuex": "^3.5.1" 29 | }, 30 | "dependencies": { 31 | "chart.js": "^2.9.4", 32 | "codemirror": "^5.63.1", 33 | "vue-chartjs": "^3.5.1", 34 | "vue-grid-layout": "^2.3.12" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /public/app.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * Chart.js v2.9.4 3 | * https://www.chartjs.org 4 | * (c) 2020 Chart.js Contributors 5 | * Released under the MIT License 6 | */ 7 | 8 | /*! 9 | * Vue.js v2.6.14 10 | * (c) 2014-2021 Evan You 11 | * Released under the MIT License. 12 | */ 13 | 14 | /*! 15 | * vuex v3.6.2 16 | * (c) 2021 Evan You 17 | * @license MIT 18 | */ 19 | 20 | /*! vue-grid-layout - 2.3.12 | (c) 2015, 2021 Gustavo Santos (JBay Solutions) (http://www.jbaysolutions.com) | https://github.com/jbaysolutions/vue-grid-layout */ 21 | 22 | /** 23 | * @license 24 | * Lodash 25 | * Copyright OpenJS Foundation and other contributors 26 | * Released under MIT license 27 | * Based on Underscore.js 1.8.3 28 | * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 29 | */ 30 | 31 | //! Copyright (c) JS Foundation and other contributors 32 | 33 | //! github.com/moment/moment-timezone 34 | 35 | //! license : MIT 36 | 37 | //! moment-timezone.js 38 | 39 | //! moment.js 40 | 41 | //! moment.js locale configuration 42 | 43 | //! version : 0.5.33 44 | -------------------------------------------------------------------------------- /public/mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/app.js": "/app.js?id=aa616d87ff4d6e21e031", 3 | "/app.css": "/app.css?id=9a7dcabdfd190adb0576" 4 | } 5 | -------------------------------------------------------------------------------- /public/resources_js_views_dashboard_vue.js: -------------------------------------------------------------------------------- 1 | (self["webpackChunk"] = self["webpackChunk"] || []).push([["resources_js_views_dashboard_vue"],{ 2 | 3 | /***/ "./node_modules/babel-loader/lib/index.js??clonedRuleSet-5[0].rules[0].use[0]!./node_modules/vue-loader/lib/index.js??vue-loader-options!./resources/js/views/dashboard.vue?vue&type=script&lang=js&": 4 | /*!***********************************************************************************************************************************************************************************************************!*\ 5 | !*** ./node_modules/babel-loader/lib/index.js??clonedRuleSet-5[0].rules[0].use[0]!./node_modules/vue-loader/lib/index.js??vue-loader-options!./resources/js/views/dashboard.vue?vue&type=script&lang=js& ***! 6 | \***********************************************************************************************************************************************************************************************************/ 7 | /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 8 | 9 | "use strict"; 10 | __webpack_require__.r(__webpack_exports__); 11 | /* harmony export */ __webpack_require__.d(__webpack_exports__, { 12 | /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) 13 | /* harmony export */ }); 14 | // 15 | // 16 | // 17 | // 18 | // 19 | // 20 | // 21 | /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ({ 22 | name: 'Dashboard' 23 | }); 24 | 25 | /***/ }), 26 | 27 | /***/ "./resources/js/views/dashboard.vue": 28 | /*!******************************************!*\ 29 | !*** ./resources/js/views/dashboard.vue ***! 30 | \******************************************/ 31 | /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 32 | 33 | "use strict"; 34 | __webpack_require__.r(__webpack_exports__); 35 | /* harmony export */ __webpack_require__.d(__webpack_exports__, { 36 | /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) 37 | /* harmony export */ }); 38 | /* harmony import */ var _dashboard_vue_vue_type_template_id_7eb83ab6___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./dashboard.vue?vue&type=template&id=7eb83ab6& */ "./resources/js/views/dashboard.vue?vue&type=template&id=7eb83ab6&"); 39 | /* harmony import */ var _dashboard_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./dashboard.vue?vue&type=script&lang=js& */ "./resources/js/views/dashboard.vue?vue&type=script&lang=js&"); 40 | /* harmony import */ var _node_modules_vue_loader_lib_runtime_componentNormalizer_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! !../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js */ "./node_modules/vue-loader/lib/runtime/componentNormalizer.js"); 41 | 42 | 43 | 44 | 45 | 46 | /* normalize component */ 47 | ; 48 | var component = (0,_node_modules_vue_loader_lib_runtime_componentNormalizer_js__WEBPACK_IMPORTED_MODULE_2__.default)( 49 | _dashboard_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__.default, 50 | _dashboard_vue_vue_type_template_id_7eb83ab6___WEBPACK_IMPORTED_MODULE_0__.render, 51 | _dashboard_vue_vue_type_template_id_7eb83ab6___WEBPACK_IMPORTED_MODULE_0__.staticRenderFns, 52 | false, 53 | null, 54 | null, 55 | null 56 | 57 | ) 58 | 59 | /* hot reload */ 60 | if (false) { var api; } 61 | component.options.__file = "resources/js/views/dashboard.vue" 62 | /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (component.exports); 63 | 64 | /***/ }), 65 | 66 | /***/ "./resources/js/views/dashboard.vue?vue&type=script&lang=js&": 67 | /*!*******************************************************************!*\ 68 | !*** ./resources/js/views/dashboard.vue?vue&type=script&lang=js& ***! 69 | \*******************************************************************/ 70 | /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 71 | 72 | "use strict"; 73 | __webpack_require__.r(__webpack_exports__); 74 | /* harmony export */ __webpack_require__.d(__webpack_exports__, { 75 | /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) 76 | /* harmony export */ }); 77 | /* harmony import */ var _node_modules_babel_loader_lib_index_js_clonedRuleSet_5_0_rules_0_use_0_node_modules_vue_loader_lib_index_js_vue_loader_options_dashboard_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-5[0].rules[0].use[0]!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./dashboard.vue?vue&type=script&lang=js& */ "./node_modules/babel-loader/lib/index.js??clonedRuleSet-5[0].rules[0].use[0]!./node_modules/vue-loader/lib/index.js??vue-loader-options!./resources/js/views/dashboard.vue?vue&type=script&lang=js&"); 78 | /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (_node_modules_babel_loader_lib_index_js_clonedRuleSet_5_0_rules_0_use_0_node_modules_vue_loader_lib_index_js_vue_loader_options_dashboard_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__.default); 79 | 80 | /***/ }), 81 | 82 | /***/ "./resources/js/views/dashboard.vue?vue&type=template&id=7eb83ab6&": 83 | /*!*************************************************************************!*\ 84 | !*** ./resources/js/views/dashboard.vue?vue&type=template&id=7eb83ab6& ***! 85 | \*************************************************************************/ 86 | /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 87 | 88 | "use strict"; 89 | __webpack_require__.r(__webpack_exports__); 90 | /* harmony export */ __webpack_require__.d(__webpack_exports__, { 91 | /* harmony export */ "render": () => (/* reexport safe */ _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_dashboard_vue_vue_type_template_id_7eb83ab6___WEBPACK_IMPORTED_MODULE_0__.render), 92 | /* harmony export */ "staticRenderFns": () => (/* reexport safe */ _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_dashboard_vue_vue_type_template_id_7eb83ab6___WEBPACK_IMPORTED_MODULE_0__.staticRenderFns) 93 | /* harmony export */ }); 94 | /* harmony import */ var _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_dashboard_vue_vue_type_template_id_7eb83ab6___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./dashboard.vue?vue&type=template&id=7eb83ab6& */ "./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib/index.js??vue-loader-options!./resources/js/views/dashboard.vue?vue&type=template&id=7eb83ab6&"); 95 | 96 | 97 | /***/ }), 98 | 99 | /***/ "./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib/index.js??vue-loader-options!./resources/js/views/dashboard.vue?vue&type=template&id=7eb83ab6&": 100 | /*!****************************************************************************************************************************************************************************************************************!*\ 101 | !*** ./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib/index.js??vue-loader-options!./resources/js/views/dashboard.vue?vue&type=template&id=7eb83ab6& ***! 102 | \****************************************************************************************************************************************************************************************************************/ 103 | /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 104 | 105 | "use strict"; 106 | __webpack_require__.r(__webpack_exports__); 107 | /* harmony export */ __webpack_require__.d(__webpack_exports__, { 108 | /* harmony export */ "render": () => (/* binding */ render), 109 | /* harmony export */ "staticRenderFns": () => (/* binding */ staticRenderFns) 110 | /* harmony export */ }); 111 | var render = function() { 112 | var _vm = this 113 | var _h = _vm.$createElement 114 | var _c = _vm._self._c || _h 115 | return _vm._m(0) 116 | } 117 | var staticRenderFns = [ 118 | function() { 119 | var _vm = this 120 | var _h = _vm.$createElement 121 | var _c = _vm._self._c || _h 122 | return _c("div", { staticClass: "text-center" }, [ 123 | _c("h3", [_vm._v("404")]), 124 | _vm._v(" "), 125 | _c("p", [_vm._v("Page was not found.")]) 126 | ]) 127 | } 128 | ] 129 | render._withStripped = true 130 | 131 | 132 | 133 | /***/ }) 134 | 135 | }]); -------------------------------------------------------------------------------- /public/resources_js_views_errors_404_vue.js: -------------------------------------------------------------------------------- 1 | (self["webpackChunk"] = self["webpackChunk"] || []).push([["resources_js_views_errors_404_vue"],{ 2 | 3 | /***/ "./node_modules/babel-loader/lib/index.js??clonedRuleSet-5[0].rules[0].use[0]!./node_modules/vue-loader/lib/index.js??vue-loader-options!./resources/js/views/errors/404.vue?vue&type=script&lang=js&": 4 | /*!************************************************************************************************************************************************************************************************************!*\ 5 | !*** ./node_modules/babel-loader/lib/index.js??clonedRuleSet-5[0].rules[0].use[0]!./node_modules/vue-loader/lib/index.js??vue-loader-options!./resources/js/views/errors/404.vue?vue&type=script&lang=js& ***! 6 | \************************************************************************************************************************************************************************************************************/ 7 | /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 8 | 9 | "use strict"; 10 | __webpack_require__.r(__webpack_exports__); 11 | /* harmony export */ __webpack_require__.d(__webpack_exports__, { 12 | /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) 13 | /* harmony export */ }); 14 | // 15 | // 16 | // 17 | // 18 | // 19 | // 20 | // 21 | /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = ({ 22 | name: '404' 23 | }); 24 | 25 | /***/ }), 26 | 27 | /***/ "./resources/js/views/errors/404.vue": 28 | /*!*******************************************!*\ 29 | !*** ./resources/js/views/errors/404.vue ***! 30 | \*******************************************/ 31 | /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 32 | 33 | "use strict"; 34 | __webpack_require__.r(__webpack_exports__); 35 | /* harmony export */ __webpack_require__.d(__webpack_exports__, { 36 | /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) 37 | /* harmony export */ }); 38 | /* harmony import */ var _404_vue_vue_type_template_id_3a5c70d3___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./404.vue?vue&type=template&id=3a5c70d3& */ "./resources/js/views/errors/404.vue?vue&type=template&id=3a5c70d3&"); 39 | /* harmony import */ var _404_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./404.vue?vue&type=script&lang=js& */ "./resources/js/views/errors/404.vue?vue&type=script&lang=js&"); 40 | /* harmony import */ var _node_modules_vue_loader_lib_runtime_componentNormalizer_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! !../../../../node_modules/vue-loader/lib/runtime/componentNormalizer.js */ "./node_modules/vue-loader/lib/runtime/componentNormalizer.js"); 41 | 42 | 43 | 44 | 45 | 46 | /* normalize component */ 47 | ; 48 | var component = (0,_node_modules_vue_loader_lib_runtime_componentNormalizer_js__WEBPACK_IMPORTED_MODULE_2__.default)( 49 | _404_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_1__.default, 50 | _404_vue_vue_type_template_id_3a5c70d3___WEBPACK_IMPORTED_MODULE_0__.render, 51 | _404_vue_vue_type_template_id_3a5c70d3___WEBPACK_IMPORTED_MODULE_0__.staticRenderFns, 52 | false, 53 | null, 54 | null, 55 | null 56 | 57 | ) 58 | 59 | /* hot reload */ 60 | if (false) { var api; } 61 | component.options.__file = "resources/js/views/errors/404.vue" 62 | /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (component.exports); 63 | 64 | /***/ }), 65 | 66 | /***/ "./resources/js/views/errors/404.vue?vue&type=script&lang=js&": 67 | /*!********************************************************************!*\ 68 | !*** ./resources/js/views/errors/404.vue?vue&type=script&lang=js& ***! 69 | \********************************************************************/ 70 | /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 71 | 72 | "use strict"; 73 | __webpack_require__.r(__webpack_exports__); 74 | /* harmony export */ __webpack_require__.d(__webpack_exports__, { 75 | /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__) 76 | /* harmony export */ }); 77 | /* harmony import */ var _node_modules_babel_loader_lib_index_js_clonedRuleSet_5_0_rules_0_use_0_node_modules_vue_loader_lib_index_js_vue_loader_options_404_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../../node_modules/babel-loader/lib/index.js??clonedRuleSet-5[0].rules[0].use[0]!../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./404.vue?vue&type=script&lang=js& */ "./node_modules/babel-loader/lib/index.js??clonedRuleSet-5[0].rules[0].use[0]!./node_modules/vue-loader/lib/index.js??vue-loader-options!./resources/js/views/errors/404.vue?vue&type=script&lang=js&"); 78 | /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (_node_modules_babel_loader_lib_index_js_clonedRuleSet_5_0_rules_0_use_0_node_modules_vue_loader_lib_index_js_vue_loader_options_404_vue_vue_type_script_lang_js___WEBPACK_IMPORTED_MODULE_0__.default); 79 | 80 | /***/ }), 81 | 82 | /***/ "./resources/js/views/errors/404.vue?vue&type=template&id=3a5c70d3&": 83 | /*!**************************************************************************!*\ 84 | !*** ./resources/js/views/errors/404.vue?vue&type=template&id=3a5c70d3& ***! 85 | \**************************************************************************/ 86 | /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 87 | 88 | "use strict"; 89 | __webpack_require__.r(__webpack_exports__); 90 | /* harmony export */ __webpack_require__.d(__webpack_exports__, { 91 | /* harmony export */ "render": () => (/* reexport safe */ _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_404_vue_vue_type_template_id_3a5c70d3___WEBPACK_IMPORTED_MODULE_0__.render), 92 | /* harmony export */ "staticRenderFns": () => (/* reexport safe */ _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_404_vue_vue_type_template_id_3a5c70d3___WEBPACK_IMPORTED_MODULE_0__.staticRenderFns) 93 | /* harmony export */ }); 94 | /* harmony import */ var _node_modules_vue_loader_lib_loaders_templateLoader_js_vue_loader_options_node_modules_vue_loader_lib_index_js_vue_loader_options_404_vue_vue_type_template_id_3a5c70d3___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! -!../../../../node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!../../../../node_modules/vue-loader/lib/index.js??vue-loader-options!./404.vue?vue&type=template&id=3a5c70d3& */ "./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib/index.js??vue-loader-options!./resources/js/views/errors/404.vue?vue&type=template&id=3a5c70d3&"); 95 | 96 | 97 | /***/ }), 98 | 99 | /***/ "./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib/index.js??vue-loader-options!./resources/js/views/errors/404.vue?vue&type=template&id=3a5c70d3&": 100 | /*!*****************************************************************************************************************************************************************************************************************!*\ 101 | !*** ./node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!./node_modules/vue-loader/lib/index.js??vue-loader-options!./resources/js/views/errors/404.vue?vue&type=template&id=3a5c70d3& ***! 102 | \*****************************************************************************************************************************************************************************************************************/ 103 | /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { 104 | 105 | "use strict"; 106 | __webpack_require__.r(__webpack_exports__); 107 | /* harmony export */ __webpack_require__.d(__webpack_exports__, { 108 | /* harmony export */ "render": () => (/* binding */ render), 109 | /* harmony export */ "staticRenderFns": () => (/* binding */ staticRenderFns) 110 | /* harmony export */ }); 111 | var render = function() { 112 | var _vm = this 113 | var _h = _vm.$createElement 114 | var _c = _vm._self._c || _h 115 | return _vm._m(0) 116 | } 117 | var staticRenderFns = [ 118 | function() { 119 | var _vm = this 120 | var _h = _vm.$createElement 121 | var _c = _vm._self._c || _h 122 | return _c("div", { staticClass: "text-center" }, [ 123 | _c("h3", [_vm._v("404")]), 124 | _vm._v(" "), 125 | _c("p", [_vm._v("Page was not found.")]) 126 | ]) 127 | } 128 | ] 129 | render._withStripped = true 130 | 131 | 132 | 133 | /***/ }) 134 | 135 | }]); -------------------------------------------------------------------------------- /resources/js/app.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import store from './store' 3 | import router from './router' 4 | import App from './components/App' 5 | import Base from './base' 6 | 7 | import './components/common' 8 | import './components/forms' 9 | 10 | Vue.config.productionTip = false 11 | 12 | Vue.mixin(Base); 13 | 14 | /* eslint-disable no-new */ 15 | new Vue({ 16 | store, 17 | router, 18 | ...App 19 | }) 20 | -------------------------------------------------------------------------------- /resources/js/base.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import moment from 'moment-timezone'; 3 | 4 | /** 5 | * Base mixin for all Vue components 6 | */ 7 | export default { 8 | computed: { 9 | apiPath() { 10 | return '/' + window.ModelStats.config.path + '/api/'; 11 | } 12 | }, 13 | 14 | methods: { 15 | /** 16 | * Show the time ago format for the given time. 17 | */ 18 | timeAgo(time) { 19 | moment.updateLocale('en', { 20 | relativeTime: { 21 | future: 'in %s', 22 | past: '%s ago', 23 | s: (number) => number + 's ago', 24 | ss: '%ds ago', 25 | m: '1m ago', 26 | mm: '%dm ago', 27 | h: '1h ago', 28 | hh: '%dh ago', 29 | d: '1d ago', 30 | dd: '%dd ago', 31 | M: 'a month ago', 32 | MM: '%d months ago', 33 | y: 'a year ago', 34 | yy: '%d years ago', 35 | }, 36 | }); 37 | 38 | let secondsElapsed = moment().diff(time, 'seconds'); 39 | let dayStart = moment('2018-01-01').startOf('day').seconds(secondsElapsed); 40 | 41 | if (secondsElapsed > 300) { 42 | return moment(time).fromNow(true); 43 | } else if (secondsElapsed < 60) { 44 | return dayStart.format('s') + 's ago'; 45 | } else { 46 | return dayStart.format('m:ss') + 'm ago'; 47 | } 48 | }, 49 | 50 | /** 51 | * Show the time in local time. 52 | */ 53 | localTime(time) { 54 | return moment(time).local().format('MMMM Do YYYY, h:mm:ss A'); 55 | }, 56 | 57 | /** 58 | * Truncate the given string. 59 | */ 60 | truncate(string, length = 70) { 61 | return _.truncate(string, { 62 | length: length, 63 | separator: /,? +/, 64 | }); 65 | }, 66 | 67 | /** 68 | * Creates a debounced function that delays invoking a callback. 69 | */ 70 | debouncer: _.debounce((callback) => callback(), 500), 71 | 72 | /** 73 | * Show an error message. 74 | */ 75 | alertError(message) { 76 | this.$root.alert.type = 'error'; 77 | this.$root.alert.autoClose = false; 78 | this.$root.alert.message = message; 79 | }, 80 | 81 | /** 82 | * Show a success message. 83 | */ 84 | alertSuccess(message, autoClose) { 85 | this.$root.alert.type = 'success'; 86 | this.$root.alert.autoClose = autoClose; 87 | this.$root.alert.message = message; 88 | }, 89 | 90 | /** 91 | * Show confirmation message. 92 | */ 93 | alertConfirm(message, success, failure) { 94 | this.$root.alert.type = 'confirmation'; 95 | this.$root.alert.autoClose = false; 96 | this.$root.alert.message = message; 97 | this.$root.alert.confirmationProceed = success; 98 | this.$root.alert.confirmationCancel = failure; 99 | }, 100 | }, 101 | }; 102 | -------------------------------------------------------------------------------- /resources/js/components/App.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 95 | -------------------------------------------------------------------------------- /resources/js/components/Layout.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 33 | -------------------------------------------------------------------------------- /resources/js/components/Loading.vue: -------------------------------------------------------------------------------- 1 | 10 | 86 | 87 | 101 | -------------------------------------------------------------------------------- /resources/js/components/charts/BarChart.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 35 | -------------------------------------------------------------------------------- /resources/js/components/charts/LineChart.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 35 | -------------------------------------------------------------------------------- /resources/js/components/common/Alert.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 82 | -------------------------------------------------------------------------------- /resources/js/components/common/VButton.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 101 | -------------------------------------------------------------------------------- /resources/js/components/common/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | 3 | import VButton from "./VButton"; 4 | import Alert from "./Alert"; 5 | 6 | // Components that are registered globaly. 7 | [ 8 | Alert, 9 | VButton 10 | ].forEach(Component => { 11 | Vue.component(Component.name, Component) 12 | }) 13 | -------------------------------------------------------------------------------- /resources/js/components/custom-code/CodeInput.vue: -------------------------------------------------------------------------------- 1 |