├── .eslintignore ├── resources ├── css │ └── app.css ├── js │ ├── components │ │ ├── Label.vue │ │ ├── icons │ │ │ ├── ChevronLeft.vue │ │ │ ├── ChevronRight.vue │ │ │ ├── Table.vue │ │ │ ├── Sizable.js │ │ │ ├── Database.vue │ │ │ ├── Eye.vue │ │ │ ├── Cog.vue │ │ │ └── Loader.vue │ │ ├── Input.vue │ │ ├── Button.vue │ │ ├── SecondaryButton.vue │ │ ├── SqlEditor.vue │ │ ├── Loader.vue │ │ ├── DialogModal.vue │ │ ├── DataCell.vue │ │ ├── SideNavigation.vue │ │ ├── Modal.vue │ │ ├── DataTable.vue │ │ └── TableStructure.vue │ ├── base.js │ ├── app.js │ ├── router.js │ ├── components.js │ └── views │ │ ├── SqlQuery.vue │ │ ├── Dashboard.vue │ │ └── TableDetails.vue └── views │ ├── partials │ └── logo.blade.php │ └── app.blade.php ├── .husky └── pre-commit ├── src ├── TableIndex.php ├── InformationSchema.php ├── Http │ ├── Middleware │ │ ├── EnsureUserIsAuthorized.php │ │ └── EnsureUpToDateAssets.php │ └── Controllers │ │ ├── TableRowsController.php │ │ ├── SqlQueryController.php │ │ ├── SelectConnectionController.php │ │ └── HomeController.php ├── Console │ ├── PublishCommand.php │ └── InstallCommand.php ├── DBObject.php ├── DatabaseRepositoryFactory.php ├── Contracts │ └── DatabaseRepository.php ├── DibiApplicationServiceProvider.php ├── DibiServiceProvider.php ├── Table.php ├── TableColumn.php ├── Repositories │ ├── SqlsrvDatabaseRepository.php │ ├── MysqlDatabaseRepository.php │ └── AbstractDatabaseRepository.php └── Dibi.php ├── volar.config.js ├── postcss.config.js ├── jsconfig.json ├── public ├── mix-manifest.json ├── app.js.LICENSE.txt └── app.css ├── tailwind.config.js ├── routes └── web.php ├── .eslintrc.js ├── stubs └── DibiServiceProvider.stub ├── phpunit.xml.dist ├── LICENSE.md ├── webpack.mix.js ├── config └── dibi.php ├── composer.json ├── package.json ├── README.md └── .php-cs-fixer.dist.php /.eslintignore: -------------------------------------------------------------------------------- 1 | public 2 | node_modules 3 | vendor 4 | -------------------------------------------------------------------------------- /resources/css/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx lint-staged 5 | -------------------------------------------------------------------------------- /src/TableIndex.php: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 18 | -------------------------------------------------------------------------------- /src/InformationSchema.php: -------------------------------------------------------------------------------- 1 | tables = $tables; 14 | } 15 | 16 | public function jsonSerialize() 17 | { 18 | return [ 19 | 'tables' => $this->tables, 20 | ]; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /resources/views/partials/logo.blade.php: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const defaultTheme = require('tailwindcss/defaultTheme'); 2 | 3 | module.exports = { 4 | content: [ 5 | './resources/**/*.blade.php', 6 | './resources/**/*.{js,ts}', 7 | './resources/**/*.vue', 8 | ], 9 | 10 | theme: { 11 | extend: { 12 | fontFamily: { 13 | sans: ['Figtree', ...defaultTheme.fontFamily.sans], 14 | }, 15 | }, 16 | }, 17 | 18 | plugins: [require('@tailwindcss/forms')], 19 | }; 20 | -------------------------------------------------------------------------------- /src/Http/Middleware/EnsureUserIsAuthorized.php: -------------------------------------------------------------------------------- 1 | json(Dibi::databaseRepository()->rows($table, $request)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Http/Controllers/SqlQueryController.php: -------------------------------------------------------------------------------- 1 | collect(explode(';', $request->sql_query))->filter()->map(function ($query) { 15 | return Dibi::databaseRepository()->runSqlQuery($query); 16 | }), 17 | ]; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /resources/js/components/icons/ChevronLeft.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 25 | -------------------------------------------------------------------------------- /resources/js/components/icons/ChevronRight.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 25 | -------------------------------------------------------------------------------- /resources/js/components/icons/Table.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 22 | -------------------------------------------------------------------------------- /resources/js/components/icons/Sizable.js: -------------------------------------------------------------------------------- 1 | export default { 2 | props: { 3 | size: { 4 | default: '8', 5 | }, 6 | }, 7 | 8 | computed: { 9 | sizeClass() { 10 | return { 11 | 2: 'h-2 w-2', 12 | 3: 'h-3 w-3', 13 | 4: 'h-4 w-4', 14 | 5: 'h-5 w-5', 15 | 6: 'h-6 w-6', 16 | 8: 'h-8 w-8', 17 | 12: 'h-12 w-12', 18 | 16: 'h-16 w-16', 19 | 20: 'h-20 w-20', 20 | }[this.size]; 21 | }, 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /resources/js/components/icons/Database.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 20 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | prefix(Dibi::path()) 8 | ->middleware(config('dibi.middleware', [])) 9 | ->group(function () { 10 | Route::post('/api/select-connection', 'SelectConnectionController@select'); 11 | Route::post('/api/sql-query', 'SqlQueryController@run'); 12 | Route::post('/api/tables/{table}/rows/filter', 'TableRowsController@filter'); 13 | 14 | Route::get('/{view?}', 'HomeController@index')->where('view', '(.*)')->name('dibi'); 15 | }); 16 | -------------------------------------------------------------------------------- /resources/js/components/Input.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 26 | -------------------------------------------------------------------------------- /resources/js/components/Button.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | -------------------------------------------------------------------------------- /resources/js/components/SecondaryButton.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | -------------------------------------------------------------------------------- /resources/js/components/icons/Eye.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 25 | -------------------------------------------------------------------------------- /src/Http/Controllers/SelectConnectionController.php: -------------------------------------------------------------------------------- 1 | connection, Dibi::databaseConnections())) { 16 | throw ValidationException::withMessages([ 17 | 'connection' => ['Invalid connection.'], 18 | ]); 19 | } 20 | 21 | Cache::put('dibiConnection', $request->connection); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /resources/js/base.js: -------------------------------------------------------------------------------- 1 | import numeral from 'numeral'; 2 | 3 | export default { 4 | methods: { 5 | formatNumber(number, format = '0,0') { 6 | return numeral(number).format(format); 7 | }, 8 | 9 | strLimit(value, limit = 55, end = '...') { 10 | if (value.length <= limit) { 11 | return value; 12 | } 13 | 14 | return `${value.substr(0, limit)}${end}`; 15 | }, 16 | 17 | httpBuildQuery(params) { 18 | const query = new URLSearchParams(); 19 | for (const [key, value] of Object.entries(params)) { 20 | query.append(key, value); 21 | } 22 | return query.toString(); 23 | }, 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /public/app.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * Vue.js v2.7.14 3 | * (c) 2014-2022 Evan You 4 | * Released under the MIT License. 5 | */ 6 | 7 | /*! @preserve 8 | * numeral.js 9 | * version : 2.0.6 10 | * author : Adam Draper 11 | * license : MIT 12 | * http://adamwdraper.github.com/Numeral-js/ 13 | */ 14 | 15 | /*! regenerator-runtime -- Copyright (c) 2014-present, Facebook, Inc. -- license (MIT): https://github.com/facebook/regenerator/blob/main/LICENSE */ 16 | 17 | /** 18 | * @license 19 | * Lodash 20 | * Copyright OpenJS Foundation and other contributors 21 | * Released under MIT license 22 | * Based on Underscore.js 1.8.3 23 | * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 24 | */ 25 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | node: true, 5 | es6: true, 6 | }, 7 | extends: [ 8 | 'plugin:vue/recommended', 9 | ], 10 | ignorePatterns: [ 11 | 'node_modules/', 12 | 'public/', 13 | 'vendor/', 14 | '*.json', 15 | ], 16 | rules: { 17 | 'vue/multi-word-component-names': ['off'], 18 | 'vue/require-prop-types': ['off'], 19 | 'vue/html-indent': ['error', 4], 20 | 'indent': ['error', 4], 21 | 'semi': ['error', 'always'], 22 | 'quotes': ['error', 'single'], 23 | 'comma-dangle': ['error', 'always-multiline'], 24 | 'object-curly-spacing': ['error', 'always'], 25 | 'comma-spacing': ['error', { 'before': false, 'after': true }], 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /src/Console/PublishCommand.php: -------------------------------------------------------------------------------- 1 | call('vendor:publish', [ 31 | '--tag' => 'dibi-assets', 32 | '--force' => true, 33 | ]); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Http/Controllers/HomeController.php: -------------------------------------------------------------------------------- 1 | Dibi::scriptVariables([ 19 | 'databaseConnections' => Dibi::databaseConnections(), 20 | 'currentDatabaseConnection' => Dibi::currentDatabaseConnection(), 21 | 'database' => Dibi::databaseRepository()->getName(), 22 | 'informationSchema' => Dibi::databaseRepository()->informationSchema(), 23 | ]), 24 | ]); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /stubs/DibiServiceProvider.stub: -------------------------------------------------------------------------------- 1 | email, [ 34 | // 35 | ]); 36 | }); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /resources/js/components/icons/Cog.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/DBObject.php: -------------------------------------------------------------------------------- 1 | raw; 22 | } 23 | 24 | /** 25 | * Set the raw DB object array from the provider. 26 | * 27 | * @return $this 28 | */ 29 | public function setRaw(array $raw) 30 | { 31 | $this->raw = $raw; 32 | 33 | return $this; 34 | } 35 | 36 | /** 37 | * Map the given array onto the DB object's properties. 38 | * 39 | * @return $this 40 | */ 41 | public function map(array $attributes) 42 | { 43 | foreach ($attributes as $key => $value) { 44 | $this->{$key} = $value; 45 | } 46 | 47 | return $this; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/DatabaseRepositoryFactory.php: -------------------------------------------------------------------------------- 1 | getDriverName()) { 21 | case 'mysql': 22 | return new MysqlDatabaseRepository($db); 23 | case 'sqlsrv': 24 | return new SqlsrvDatabaseRepository($db); 25 | default: 26 | throw new InvalidArgumentException('Database driver ['.$db->getDriverName().'] is not supported.'); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /resources/js/components/SqlEditor.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 38 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 16 | 17 | 18 | ./tests 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Cuong Giang 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 | -------------------------------------------------------------------------------- /resources/js/components/Loader.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 46 | -------------------------------------------------------------------------------- /src/Http/Middleware/EnsureUpToDateAssets.php: -------------------------------------------------------------------------------- 1 | runningInConsole()) { 22 | $publishedPath = public_path('vendor/dibi/mix-manifest.json'); 23 | 24 | if (! File::exists($publishedPath)) { 25 | throw new RuntimeException('The Dibi assets are not published. Please run: `php artisan dibi:install`.'); 26 | } 27 | 28 | if (File::get($publishedPath) !== File::get(__DIR__.'/../../../public/mix-manifest.json')) { 29 | throw new RuntimeException('The published Dibi assets are not up-to-date with the installed version. Please run: `php artisan dibi:publish`.'); 30 | } 31 | } 32 | 33 | return $next($request); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | const mix = require('laravel-mix'); 2 | const path = require('path'); 3 | const MonacoEditorWebpackPlugin = require('monaco-editor-webpack-plugin'); 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 17 | .webpackConfig({ 18 | plugins: [ 19 | new MonacoEditorWebpackPlugin({ 20 | // available options are documented at https://github.com/Microsoft/monaco-editor-webpack-plugin#options 21 | languages: ['sql'], 22 | }), 23 | ], 24 | }) 25 | .js('resources/js/app.js', 'public') 26 | .vue({ version: 2 }) 27 | .setPublicPath('public') 28 | .postCss('resources/css/app.css', 'public', [ 29 | require('tailwindcss'), 30 | ]) 31 | .alias({ '@': path.join(__dirname, 'resources/js/') }) 32 | .version(); 33 | -------------------------------------------------------------------------------- /config/dibi.php: -------------------------------------------------------------------------------- 1 | env('DIBI_PATH', '/dibi'), 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Dibi Route Middleware 24 | |-------------------------------------------------------------------------- 25 | | 26 | | These middleware will be assigned to every Dibi route - giving you 27 | | the chance to add your own middleware to this list or change any of 28 | | the existing middleware. Or, you can simply stick with this list. 29 | | 30 | */ 31 | 32 | 'middleware' => [ 33 | 'web', 34 | EnsureUserIsAuthorized::class, 35 | EnsureUpToDateAssets::class, 36 | ], 37 | 38 | ]; 39 | -------------------------------------------------------------------------------- /resources/js/components/DialogModal.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 57 | -------------------------------------------------------------------------------- /src/Contracts/DatabaseRepository.php: -------------------------------------------------------------------------------- 1 | authorization(); 18 | } 19 | 20 | /** 21 | * Configure the Dibi authorization services. 22 | * 23 | * @return void 24 | */ 25 | protected function authorization() 26 | { 27 | $this->gate(); 28 | 29 | Dibi::auth(function ($request) { 30 | return app()->environment('local') || 31 | Gate::check('viewDibi', [$request->user()]); 32 | }); 33 | } 34 | 35 | /** 36 | * Register the Dibi gate. 37 | * 38 | * This gate determines who can access Dibi in non-local environments. 39 | * 40 | * @return void 41 | */ 42 | protected function gate() 43 | { 44 | Gate::define('viewDibi', function ($user) { 45 | return in_array($user->email, [ 46 | // 47 | ]); 48 | }); 49 | } 50 | 51 | /** 52 | * Register any application services. 53 | * 54 | * @return void 55 | */ 56 | public function register() 57 | { 58 | // 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /resources/js/app.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import axios from 'axios'; 3 | import _ from 'lodash'; 4 | import PortalVue from 'portal-vue'; 5 | import Toasted from 'vue-toasted'; 6 | import router from './router'; 7 | import Base from './base'; 8 | 9 | Vue.config.productionTip = false; 10 | 11 | Vue.use(PortalVue); 12 | Vue.use(Toasted, { 13 | position: 'bottom-right', 14 | duration: 6000, 15 | }); 16 | 17 | window._ = _; 18 | window.Bus = new Vue({ name: 'Bus' }); 19 | 20 | window.axios = axios.create(); 21 | 22 | const token = document.head.querySelector('meta[name="csrf-token"]'); 23 | 24 | if (token) { 25 | window.axios.defaults.headers.common['X-CSRF-TOKEN'] = token.content; 26 | } 27 | 28 | window.axios.interceptors.response.use( 29 | response => response, 30 | error => { 31 | if (! error.response) { 32 | return Promise.reject(error); 33 | } 34 | 35 | const { status } = error.response; 36 | 37 | // Show the user a 500 error 38 | if (status >= 500) { 39 | Bus.$emit('error', error.response.data.message); 40 | } 41 | 42 | return Promise.reject(error); 43 | }, 44 | ); 45 | 46 | import './components'; 47 | 48 | Vue.mixin(Base); 49 | 50 | new Vue({ 51 | el: '#dibi', 52 | router, 53 | mounted() { 54 | Bus.$on('error', message => { 55 | this.$toasted.show(message, { type: 'error' }); 56 | }); 57 | }, 58 | }); 59 | -------------------------------------------------------------------------------- /src/DibiServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadRoutesFrom(__DIR__.'/../routes/web.php'); 17 | 18 | $this->loadViewsFrom(__DIR__.'/../resources/views', 'dibi'); 19 | 20 | if ($this->app->runningInConsole()) { 21 | $this->publishes([ 22 | __DIR__.'/../config/dibi.php' => config_path('dibi.php'), 23 | ], 'dibi-config'); 24 | 25 | $this->publishes([ 26 | __DIR__.'/../public' => public_path('vendor/dibi'), 27 | ], 'dibi-assets'); 28 | 29 | $this->publishes([ 30 | __DIR__.'/../stubs/DibiServiceProvider.stub' => app_path('Providers/DibiServiceProvider.php'), 31 | ], 'dibi-provider'); 32 | } 33 | } 34 | 35 | /** 36 | * Register any application services. 37 | * 38 | * @return void 39 | */ 40 | public function register() 41 | { 42 | $this->mergeConfigFrom( 43 | __DIR__.'/../config/dibi.php', 'dibi' 44 | ); 45 | 46 | $this->commands([ 47 | Console\InstallCommand::class, 48 | Console\PublishCommand::class, 49 | ]); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cuonggt/laravel-dibi", 3 | "description": "An elegant GUI database management tool for your Laravel applications.", 4 | "keywords": [ 5 | "laravel", 6 | "dibi" 7 | ], 8 | "homepage": "https://github.com/cuonggt/laravel-dibi", 9 | "license": "MIT", 10 | "support": { 11 | "issues": "https://github.com/cuonggt/laravel-dibi/issues", 12 | "source": "https://github.com/cuonggt/laravel-dibi" 13 | }, 14 | "authors": [ 15 | { 16 | "name": "Cuong Giang", 17 | "email": "thaicuong.giang@gmail.com" 18 | } 19 | ], 20 | "require": { 21 | "php": "^7.3|^8.0.2|^8.3", 22 | "ext-json": "*", 23 | "laravel/framework": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 24 | "ramsey/uuid": "^3.8|^4.0" 25 | }, 26 | "require-dev": { 27 | "friendsofphp/php-cs-fixer": "^3.4", 28 | "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0|^8.0|^9.0|^10.0" 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "Cuonggt\\Dibi\\": "src/" 33 | } 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "Cuonggt\\Dibi\\Tests\\": "tests/" 38 | } 39 | }, 40 | "extra": { 41 | "laravel": { 42 | "providers": [ 43 | "Cuonggt\\Dibi\\DibiServiceProvider" 44 | ] 45 | } 46 | }, 47 | "config": { 48 | "sort-packages": true 49 | }, 50 | "minimum-stability": "dev", 51 | "prefer-stable": true 52 | } 53 | -------------------------------------------------------------------------------- /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 | "lint": "eslint --cache --ext=js,ts,vue", 12 | "lint:fix": "eslint --cache --ext=js,ts,vue --fix", 13 | "prepare": "husky install" 14 | }, 15 | "devDependencies": { 16 | "@monaco-editor/loader": "^1.3.3", 17 | "@tailwindcss/forms": "^0.5.3", 18 | "@volar-plugins/vetur": "^2.0.0", 19 | "autoprefixer": "^10.4.13", 20 | "axios": "^1.6.0", 21 | "cross-env": "^7.0.3", 22 | "eslint": "^8.34.0", 23 | "eslint-plugin-vue": "^9.9.0", 24 | "husky": "^8.0.3", 25 | "laravel-mix": "^6.0.0", 26 | "lint-staged": "^13.1.2", 27 | "lodash": "^4.17.21", 28 | "monaco-editor": "^0.39.0", 29 | "monaco-editor-webpack-plugin": "^7.0.1", 30 | "numeral": "^2.0.6", 31 | "portal-vue": "^2.1.7", 32 | "postcss": "^8.4.31", 33 | "splitpanes": "^2.4.1", 34 | "tailwindcss": "^3.2.7", 35 | "vue": "^2.6.12", 36 | "vue-json-pretty": "^1.9.4", 37 | "vue-loader": "^15.9.8", 38 | "vue-router": "^3.5.1", 39 | "vue-template-compiler": "^2.7.14", 40 | "vue-toasted": "^1.1.28" 41 | }, 42 | "lint-staged": { 43 | "**/*.{js,ts,vue}": [ 44 | "eslint --cache --fix" 45 | ], 46 | "**/*.php": [ 47 | "./vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.dist.php" 48 | ] 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Table.php: -------------------------------------------------------------------------------- 1 | columns; 48 | } 49 | 50 | $this->columns = $columns; 51 | 52 | return $this; 53 | } 54 | 55 | /** 56 | * Set the indexes of the table. 57 | * 58 | * @return $this|array 59 | */ 60 | public function indexes(array $indexes = null) 61 | { 62 | if (is_null($indexes)) { 63 | return $this->indexes; 64 | } 65 | 66 | $this->indexes = $indexes; 67 | 68 | return $this; 69 | } 70 | 71 | /** 72 | * Get the JSON serializable fields for the object. 73 | * 74 | * @return array 75 | */ 76 | public function jsonSerialize() 77 | { 78 | return [ 79 | 'tableCatalog' => $this->tableCatalog, 80 | 'tableSchema' => $this->tableSchema, 81 | 'tableName' => $this->tableName, 82 | 'tableType' => $this->tableType, 83 | 'columns' => $this->columns, 84 | 'indexes' => $this->indexes, 85 | ]; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /resources/js/router.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Router from 'vue-router'; 3 | import Dashboard from './views/Dashboard'; 4 | import TableDetails from './views/TableDetails'; 5 | import SqlQuery from './views/SqlQuery'; 6 | 7 | Vue.use(Router); 8 | 9 | const router = new Router({ 10 | scrollBehavior, 11 | base: window.Dibi.path, 12 | mode: 'history', 13 | routes: [ 14 | { 15 | path: '/', 16 | redirect: '/dashboard', 17 | }, 18 | { 19 | name: 'dashboard', 20 | path: '/dashboard', 21 | component: Dashboard, 22 | props: true, 23 | }, 24 | { 25 | name: 'sql-query', 26 | path: '/sql-query', 27 | component: SqlQuery, 28 | props: true, 29 | }, 30 | { 31 | path: '/tables/:tableName', 32 | component: TableDetails, 33 | name: 'tables-show', 34 | props: route => ({ 35 | tableName: route.params.tableName, 36 | currentTable: Dibi.informationSchema.tables.find(table => table.tableName == route.params.tableName), 37 | }), 38 | }, 39 | { 40 | name: 'catch-all', 41 | path: '*', 42 | redirect: () => { 43 | window.location.href = '/404'; 44 | }, 45 | }, 46 | ], 47 | }); 48 | 49 | export default router; 50 | 51 | function scrollBehavior(to, from, savedPosition) { 52 | if (savedPosition) { 53 | return savedPosition; 54 | } 55 | 56 | if (to.hash) { 57 | return { selector: to.hash }; 58 | } 59 | 60 | const [component] = router.getMatchedComponents({ ...to }).slice(-1); 61 | 62 | if (component && component.scrollToTop === false) { 63 | return {}; 64 | } 65 | 66 | return { x: 0, y: 0 }; 67 | } 68 | 69 | -------------------------------------------------------------------------------- /resources/js/components.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | 3 | import IconChevronLeft from './components/icons/ChevronLeft.vue'; 4 | import IconChevronRight from './components/icons/ChevronRight.vue'; 5 | import IconCog from './components/icons/Cog.vue'; 6 | import IconDatabase from './components/icons/Database.vue'; 7 | import IconEye from './components/icons/Eye.vue'; 8 | import IconLoader from './components/icons/Loader.vue'; 9 | import IconTable from './components/icons/Table.vue'; 10 | 11 | import XButton from './components/Button.vue'; 12 | import XDialogModal from './components/DialogModal.vue'; 13 | import XInput from './components/Input.vue'; 14 | import XLabel from './components/Label.vue'; 15 | import XLoader from './components/Loader.vue'; 16 | import XSecondaryButton from './components/SecondaryButton.vue'; 17 | 18 | import SideNavigation from './components/SideNavigation.vue'; 19 | import TableStructure from './components/TableStructure.vue'; 20 | import DataTable from './components/DataTable.vue'; 21 | import DataCell from './components/DataCell.vue'; 22 | import SqlEditor from './components/SqlEditor.vue'; 23 | 24 | // Icons 25 | Vue.component('IconChevronLeft', IconChevronLeft); 26 | Vue.component('IconChevronRight', IconChevronRight); 27 | Vue.component('IconCog', IconCog); 28 | Vue.component('IconDatabase', IconDatabase); 29 | Vue.component('IconEye', IconEye); 30 | Vue.component('IconLoader', IconLoader); 31 | Vue.component('IconTable', IconTable); 32 | 33 | // Components 34 | Vue.component('XButton', XButton); 35 | Vue.component('XDialogModal', XDialogModal); 36 | Vue.component('XInput', XInput); 37 | Vue.component('XLabel', XLabel); 38 | Vue.component('XLoader', XLoader); 39 | Vue.component('XSecondaryButton', XSecondaryButton); 40 | 41 | Vue.component('SideNavigation', SideNavigation); 42 | Vue.component('TableStructure', TableStructure); 43 | Vue.component('DataTable', DataTable); 44 | Vue.component('DataCell', DataCell); 45 | Vue.component('SqlEditor', SqlEditor); 46 | -------------------------------------------------------------------------------- /resources/views/app.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Dibi 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 |
21 |
22 | 35 |
36 | 37 | 38 | 39 | 40 | 41 | 42 |
43 |
44 | 45 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /resources/js/components/DataCell.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 62 | -------------------------------------------------------------------------------- /resources/js/components/SideNavigation.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 63 | -------------------------------------------------------------------------------- /src/Console/InstallCommand.php: -------------------------------------------------------------------------------- 1 | comment('Publishing Dibi Service Provider...'); 32 | $this->callSilent('vendor:publish', ['--tag' => 'dibi-provider']); 33 | 34 | $this->comment('Publishing Dibi Assets...'); 35 | $this->callSilent('vendor:publish', ['--tag' => 'dibi-assets']); 36 | 37 | $this->registerDibiServiceProvider(); 38 | 39 | $this->info('Dibi scaffolding installed successfully.'); 40 | } 41 | 42 | /** 43 | * Register the Dibi service provider in the application configuration file. 44 | * 45 | * @return void 46 | */ 47 | protected function registerDibiServiceProvider() 48 | { 49 | $namespace = Str::replaceLast('\\', '', $this->laravel->getNamespace()); 50 | 51 | $appConfig = file_get_contents(config_path('app.php')); 52 | 53 | if (! $appConfig) { 54 | return; 55 | } 56 | 57 | if (Str::contains($appConfig, $namespace.'\\Providers\\DibiServiceProvider::class')) { 58 | return; 59 | } 60 | 61 | $lineEndingCount = [ 62 | "\r\n" => substr_count($appConfig, "\r\n"), 63 | "\r" => substr_count($appConfig, "\r"), 64 | "\n" => substr_count($appConfig, "\n"), 65 | ]; 66 | 67 | $eol = array_keys($lineEndingCount, max($lineEndingCount))[0]; 68 | 69 | file_put_contents(config_path('app.php'), str_replace( 70 | "{$namespace}\\Providers\RouteServiceProvider::class,".$eol, 71 | "{$namespace}\\Providers\RouteServiceProvider::class,".$eol." {$namespace}\Providers\DibiServiceProvider::class,".$eol, 72 | $appConfig 73 | )); 74 | 75 | file_put_contents(app_path('Providers/DibiServiceProvider.php'), str_replace( 76 | "namespace App\Providers;", 77 | "namespace {$namespace}\Providers;", 78 | file_get_contents(app_path('Providers/DibiServiceProvider.php')) 79 | )); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /resources/js/components/icons/Loader.vue: -------------------------------------------------------------------------------- 1 | 88 | 89 | 96 | -------------------------------------------------------------------------------- /src/TableColumn.php: -------------------------------------------------------------------------------- 1 | $this->tableCatalog, 103 | 'tableSchema' => $this->tableSchema, 104 | 'tableName' => $this->tableName, 105 | 'columnName' => $this->columnName, 106 | 'ordinalPosition' => $this->ordinalPosition, 107 | 'columnDefault' => $this->columnDefault, 108 | 'isNullable' => $this->isNullable, 109 | 'dataType' => $this->dataType, 110 | 'characterMaximumLength' => $this->characterMaximumLength, 111 | 'characterOctetLength' => $this->characterOctetLength, 112 | 'numericPrecision' => $this->numericPrecision, 113 | 'numericScale' => $this->numericScale, 114 | 'datetimePrecision' => $this->datetimePrecision, 115 | 'characterSetName' => $this->characterSetName, 116 | 'collationName' => $this->collationName, 117 | 'isStringDataType' => $this->isStringDataType, 118 | 'shouldHideValue' => $this->isStringDataType && $this->characterMaximumLength > 255, 119 | ]; 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /resources/js/views/SqlQuery.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 92 | -------------------------------------------------------------------------------- /src/Repositories/SqlsrvDatabaseRepository.php: -------------------------------------------------------------------------------- 1 | db->select( 35 | 'SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_CATALOG = ? ORDER BY TABLE_SCHEMA ASC, TABLE_TYPE ASC, TABLE_NAME ASC', 36 | [$this->getName()] 37 | ); 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | protected function mapRawTableToObject($rawTable) 44 | { 45 | return (new Table)->setRaw((array) $rawTable)->map([ 46 | 'tableCatalog' => $rawTable->TABLE_CATALOG, 47 | 'tableSchema' => $rawTable->TABLE_SCHEMA, 48 | 'tableName' => $rawTable->TABLE_SCHEMA.'.'.$rawTable->TABLE_NAME, 49 | 'tableType' => $rawTable->TABLE_TYPE, 50 | ]); 51 | } 52 | 53 | /** 54 | * {@inheritdoc} 55 | */ 56 | protected function rawColumns() 57 | { 58 | return $this->db->select( 59 | 'SELECT * FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_CATALOG = ? ORDER BY TABLE_SCHEMA ASC, ORDINAL_POSITION ASC', 60 | [$this->getName()] 61 | ); 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | protected function mapRawColumnToObject($rawColumn) 68 | { 69 | return (new TableColumn)->setRaw((array) $rawColumn)->map([ 70 | 'tableCatalog' => $rawColumn->TABLE_CATALOG, 71 | 'tableSchema' => $rawColumn->TABLE_SCHEMA, 72 | 'tableName' => $rawColumn->TABLE_SCHEMA.'.'.$rawColumn->TABLE_NAME, 73 | 'columnName' => $rawColumn->COLUMN_NAME, 74 | 'ordinalPosition' => $rawColumn->ORDINAL_POSITION, 75 | 'columnDefault' => $rawColumn->COLUMN_DEFAULT, 76 | 'isNullable' => $rawColumn->IS_NULLABLE == 'YES', 77 | 'dataType' => $rawColumn->DATA_TYPE, 78 | 'characterMaximumLength' => $rawColumn->CHARACTER_MAXIMUM_LENGTH, 79 | 'characterOctetLength' => $rawColumn->CHARACTER_OCTET_LENGTH, 80 | 'numericPrecision' => $rawColumn->NUMERIC_PRECISION, 81 | 'numericScale' => $rawColumn->NUMERIC_SCALE, 82 | 'datetimePrecision' => $rawColumn->DATETIME_PRECISION, 83 | 'characterSetName' => $rawColumn->CHARACTER_SET_NAME, 84 | 'collationName' => $rawColumn->COLLATION_NAME, 85 | 'isStringDataType' => in_array($rawColumn->DATA_TYPE, $this->stringDataTypes()), 86 | ]); 87 | } 88 | 89 | /** 90 | * {@inheritdoc} 91 | */ 92 | protected function rawIndexes() 93 | { 94 | return []; 95 | } 96 | 97 | /** 98 | * {@inheritdoc} 99 | */ 100 | protected function mapRawIndexToObject($rawIndex) 101 | { 102 | return (new TableIndex)->setRaw((array) $rawIndex)->map([]); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /resources/js/components/Modal.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 116 | -------------------------------------------------------------------------------- /src/Dibi.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | public static $databaseConnections = []; 25 | 26 | /** 27 | * Database Connection Name. 28 | * 29 | * @var string 30 | */ 31 | public static $databaseConnectionName = 'mysql'; 32 | 33 | /** 34 | * Database Repository. 35 | * 36 | * @var \Cuonggt\Dibi\Contracts\DatabaseRepository 37 | */ 38 | public static $databaseRepository; 39 | 40 | /** 41 | * Get the URI path prefix utilized by Dibi. 42 | * 43 | * @return string 44 | */ 45 | public static function path() 46 | { 47 | return config('dibi.path', '/dibi'); 48 | } 49 | 50 | /** 51 | * Determine if the given request can access the Horizon dashboard. 52 | * 53 | * @param \Illuminate\Http\Request $request 54 | * @return bool 55 | */ 56 | public static function check($request) 57 | { 58 | return (static::$authUsing ?: function () { 59 | return app()->environment('local'); 60 | })($request); 61 | } 62 | 63 | /** 64 | * Set the callback that should be used to authenticate Horizon users. 65 | * 66 | * @return static 67 | */ 68 | public static function auth(Closure $callback) 69 | { 70 | static::$authUsing = $callback; 71 | 72 | return new static; 73 | } 74 | 75 | /** 76 | * Register available database connections. 77 | * 78 | * @return static 79 | */ 80 | public static function registerDatabaseConnections(array $databaseConnections) 81 | { 82 | static::$databaseConnections = $databaseConnections; 83 | 84 | return new static; 85 | } 86 | 87 | /** 88 | * Get the list of available database connections. 89 | * 90 | * @return array 91 | */ 92 | public static function databaseConnections() 93 | { 94 | return empty(static::$databaseConnections) ? [static::$databaseConnectionName] : static::$databaseConnections; 95 | } 96 | 97 | /** 98 | * Get the current database connection name. 99 | * 100 | * @return string 101 | */ 102 | public static function currentDatabaseConnection() 103 | { 104 | return Cache::get('dibiConnection') ?? Arr::first(static::databaseConnections()); 105 | } 106 | 107 | /** 108 | * Specify the database connection name that should be used by Dibi. 109 | * 110 | * @param string $databaseConnectionName 111 | * @return static 112 | */ 113 | public static function useDatabaseConnectionName($databaseConnectionName) 114 | { 115 | static::$databaseConnectionName = $databaseConnectionName; 116 | 117 | return new static; 118 | } 119 | 120 | /** 121 | * Get the database connection. 122 | * 123 | * @return \Illuminate\Database\Connection 124 | */ 125 | public static function databaseConnection() 126 | { 127 | return DB::connection(static::currentDatabaseConnection()); 128 | } 129 | 130 | /** 131 | * Get the database repository. 132 | * 133 | * @return \Cuonggt\Dibi\Contracts\DatabaseRepository 134 | */ 135 | public static function databaseRepository() 136 | { 137 | return static::$databaseRepository ?: static::$databaseRepository = DatabaseRepositoryFactory::make(static::databaseConnection()); 138 | } 139 | 140 | /** 141 | * Get the default JavaScript variables for Dibi. 142 | * 143 | * @param array $options 144 | * @return array 145 | */ 146 | public static function scriptVariables($options = []) 147 | { 148 | return array_merge([ 149 | 'path' => static::path(), 150 | ], $options); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/Repositories/MysqlDatabaseRepository.php: -------------------------------------------------------------------------------- 1 | db->select( 40 | 'SELECT * FROM information_schema.tables WHERE table_schema = ? ORDER BY table_name ASC', 41 | [$this->getName()] 42 | ); 43 | } 44 | 45 | /** 46 | * {@inheritdoc} 47 | */ 48 | protected function mapRawTableToObject($rawTable) 49 | { 50 | return (new Table)->setRaw((array) $rawTable)->map([ 51 | 'tableCatalog' => $rawTable->TABLE_CATALOG, 52 | 'tableSchema' => $rawTable->TABLE_SCHEMA, 53 | 'tableName' => $rawTable->TABLE_NAME, 54 | 'tableType' => $rawTable->TABLE_TYPE, 55 | ]); 56 | } 57 | 58 | /** 59 | * {@inheritdoc} 60 | */ 61 | protected function rawColumns() 62 | { 63 | return $this->db->select( 64 | 'SELECT * FROM information_schema.columns WHERE table_schema = ? ORDER BY table_name ASC, ordinal_position ASC', 65 | [$this->getName()] 66 | ); 67 | } 68 | 69 | /** 70 | * {@inheritdoc} 71 | */ 72 | protected function mapRawColumnToObject($rawColumn) 73 | { 74 | return (new TableColumn)->setRaw((array) $rawColumn)->map([ 75 | 'tableCatalog' => $rawColumn->TABLE_CATALOG, 76 | 'tableSchema' => $rawColumn->TABLE_SCHEMA, 77 | 'tableName' => $rawColumn->TABLE_NAME, 78 | 'columnName' => $rawColumn->COLUMN_NAME, 79 | 'ordinalPosition' => $rawColumn->ORDINAL_POSITION, 80 | 'columnDefault' => $rawColumn->COLUMN_DEFAULT, 81 | 'isNullable' => $rawColumn->IS_NULLABLE == 'YES', 82 | 'dataType' => $rawColumn->DATA_TYPE, 83 | 'characterMaximumLength' => $rawColumn->CHARACTER_MAXIMUM_LENGTH, 84 | 'characterOctetLength' => $rawColumn->CHARACTER_OCTET_LENGTH, 85 | 'numericPrecision' => $rawColumn->NUMERIC_PRECISION, 86 | 'numericScale' => $rawColumn->NUMERIC_SCALE, 87 | 'datetimePrecision' => $rawColumn->DATETIME_PRECISION, 88 | 'characterSetName' => $rawColumn->CHARACTER_SET_NAME, 89 | 'collationName' => $rawColumn->COLLATION_NAME, 90 | 'isStringDataType' => in_array($rawColumn->DATA_TYPE, $this->stringDataTypes()), 91 | ]); 92 | } 93 | 94 | /** 95 | * {@inheritdoc} 96 | */ 97 | protected function rawIndexes() 98 | { 99 | return $this->db->select( 100 | 'SELECT * FROM information_schema.statistics WHERE table_schema = ? ORDER BY table_name ASC', 101 | [$this->getName()] 102 | ); 103 | } 104 | 105 | /** 106 | * {@inheritdoc} 107 | */ 108 | protected function mapRawIndexToObject($rawIndex) 109 | { 110 | return (new TableIndex)->setRaw((array) $rawIndex)->map([ 111 | 'tableCatalog' => $rawIndex->TABLE_CATALOG, 112 | 'tableSchema' => $rawIndex->TABLE_SCHEMA, 113 | 'tableName' => $rawIndex->TABLE_NAME, 114 | 'nonUnique' => (bool) $rawIndex->NON_UNIQUE, 115 | 'indexSchema' => $rawIndex->INDEX_SCHEMA, 116 | 'indexName' => $rawIndex->INDEX_NAME, 117 | 'seqInIndex' => $rawIndex->SEQ_IN_INDEX, 118 | 'columnName' => $rawIndex->COLUMN_NAME, 119 | 'indexType' => $rawIndex->INDEX_TYPE, 120 | ]); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /resources/js/views/Dashboard.vue: -------------------------------------------------------------------------------- 1 | 78 | 79 | 106 | -------------------------------------------------------------------------------- /resources/js/components/DataTable.vue: -------------------------------------------------------------------------------- 1 | 92 | 93 | 129 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Dibi 2 | 3 |

4 | Build Status 5 | License 6 |

7 | 8 | ![screenshot](https://user-images.githubusercontent.com/8156596/115648495-b9360080-a34f-11eb-86c5-4da253046f86.png) 9 | 10 | Laravel Dibi is an elegant GUI database management tool for your Laravel applications. It provides a quick access for browsing your database on local/dev server without installing any other applications. 11 | 12 | ## Installation 13 | 14 | You may install Dibi into your project using the Composer package manager: 15 | 16 | ```bash 17 | composer require cuonggt/laravel-dibi 18 | ``` 19 | 20 | After installing Dibi, publish its assets using the `dibi:install` Artisan command: 21 | 22 | ```bash 23 | php artisan dibi:install 24 | ``` 25 | 26 | Currently, Dibi only supports MySQL. I hope other DB engines like SQL Server, PostgreSQL, SQLite, etc will be supported in near future. 27 | 28 | Dibi use database connection name `mysql` by default to connect. If you would like to use another database connection name, you may use the `Dibi::useDatabaseConnectionName` method. You may call this method from the `boot` method of your application's `App\Providers\DibiServiceProvider`: 29 | 30 | ```php 31 | /** 32 | * Bootstrap any application services. 33 | * 34 | * @return void 35 | */ 36 | public function boot() 37 | { 38 | parent::boot(); 39 | 40 | Dibi::useDatabaseConnectionName('custom_mysql'); 41 | } 42 | ``` 43 | 44 | ### Dashboard Authorization 45 | 46 | Dibi exposes a dashboard at the `/dibi` URI. By default, you will only be able to access this dashboard in the `local` environment. However, within your `app/Providers/DibiServiceProvider.php` file, there is an [authorization gate](https://laravel.com/docs/8.x/authorization#gates) definition. This authorization gate controls access to Dibi in **non-local** environments. You are free to modify this gate as needed to restrict access to your Dibi installation: 47 | 48 | ```php 49 | /** 50 | * Register the Dibi gate. 51 | * 52 | * This gate determines who can access Dibi in non-local environments. 53 | * 54 | * @return void 55 | */ 56 | protected function gate() 57 | { 58 | Gate::define('viewDibi', function ($user) { 59 | return in_array($user->email, [ 60 | 'admin@example.com', 61 | ]); 62 | }); 63 | } 64 | ``` 65 | 66 | ### Upgrading Dibi 67 | 68 | When upgrading to a new major version of Dibi, it's important that you carefully review the upgrade guide. In addition, when upgrading to any new Dibi version, you should re-publish Dibi's assets: 69 | 70 | ```bash 71 | php artisan dibi:publish 72 | ``` 73 | 74 | To keep the assets up-to-date and avoid issues in future updates, you may add the `dibi:publish` command to the `post-update-cmd` scripts in your application's `composer.json` file: 75 | 76 | ```json 77 | { 78 | "scripts": { 79 | "post-update-cmd": [ 80 | "@php artisan dibi:publish --ansi" 81 | ] 82 | } 83 | } 84 | ``` 85 | 86 | ### Customizing Middleware 87 | 88 | If needed, you can customize the middleware stack used by Dibi routes by updating your `config/dibi.php` file. If you have not published Dibi's confiugration file, you may do so using the `vendor:publish` Artisan command: 89 | 90 | ``` 91 | php artisan vendor:publish --tag=dibi-config 92 | ``` 93 | 94 | Once the configuration file has been published, you may edit Dibi's middleware by tweaking the `middleware` configuration option within this file: 95 | 96 | ```php 97 | /* 98 | |-------------------------------------------------------------------------- 99 | | Dibi Route Middleware 100 | |-------------------------------------------------------------------------- 101 | | 102 | | These middleware will be assigned to every Dibi route - giving you 103 | | the chance to add your own middleware to this list or change any of 104 | | the existing middleware. Or, you can simply stick with this list. 105 | | 106 | */ 107 | 108 | 'middleware' => [ 109 | 'web', 110 | EnsureUserIsAuthorized::class, 111 | EnsureUpToDateAssets::class 112 | ], 113 | ``` 114 | 115 | ### Local Only Installation 116 | 117 | If you plan to only use Dibi to assist your local development, you may install Dibi using the `--dev` flag: 118 | 119 | ```bash 120 | composer require cuonggt/laravel-dibi --dev 121 | php artisan dibi:install 122 | ``` 123 | 124 | After running `dibi:install`, you should remove the `DibiServiceProvider` service provider registration from your application's `config/app.php` configuration file. Instead, manually register Dibi's service providers in the `register` method of your `App\Providers\AppServiceProvider` class. We will ensure the current environment is one of specific environments before registering the providers: 125 | 126 | ```php 127 | /** 128 | * Register any application services. 129 | * 130 | * @return void 131 | */ 132 | public function register() 133 | { 134 | if ($this->app->environment('local', 'develop', 'staging')) { 135 | $this->app->register(\Cuonggt\Dibi\DibiServiceProvider::class); 136 | $this->app->register(DibiServiceProvider::class); 137 | } 138 | } 139 | ``` 140 | 141 | Finally, you should also prevent the Dibi package from being auto-discovered by adding the following to your `composer.json` file: 142 | 143 | ```json 144 | "extra": { 145 | "laravel": { 146 | "dont-discover": [ 147 | "cuonggt/laravel-dibi" 148 | ] 149 | } 150 | }, 151 | ``` 152 | 153 | ## License 154 | 155 | Laravel Dibi is open-sourced software licensed under the [MIT license](http://opensource.org/licenses/MIT). 156 | -------------------------------------------------------------------------------- /src/Repositories/AbstractDatabaseRepository.php: -------------------------------------------------------------------------------- 1 | db = $db; 28 | } 29 | 30 | /** 31 | * {@inheritdoc} 32 | */ 33 | public function getName() 34 | { 35 | return $this->db->getDatabaseName(); 36 | } 37 | 38 | /** 39 | * {@inheritdoc} 40 | */ 41 | public function runSqlQuery($sqlQuery) 42 | { 43 | $statement = strtolower(Arr::first(explode(' ', $sqlQuery))); 44 | 45 | if (! in_array($statement, ['select', 'insert', 'update', 'delete'])) { 46 | $statement = 'statement'; 47 | } 48 | 49 | return [ 50 | 'query' => $sqlQuery, 51 | 'statement' => $statement, 52 | 'result' => $this->db->{$statement}($sqlQuery), 53 | ]; 54 | } 55 | 56 | /** 57 | * {@inheritdoc} 58 | */ 59 | public function informationSchema() 60 | { 61 | return new InformationSchema($this->tables()); 62 | } 63 | 64 | /** 65 | * {@inheritdoc} 66 | */ 67 | public function tables() 68 | { 69 | $columns = $this->columns()->groupBy('tableName'); 70 | $indexes = $this->indexes()->groupBy('tableName'); 71 | 72 | return collect($this->rawTables()) 73 | ->map(function ($rawTable) use ($columns, $indexes) { 74 | $table = $this->mapRawTableToObject($rawTable); 75 | 76 | if ($indexes[$table->tableName] ?? false) { 77 | $table->indexes($indexes[$table->tableName]->toArray()); 78 | } 79 | 80 | return $table 81 | ->columns($columns[$table->tableName]->toArray()); 82 | }) 83 | ->toArray(); 84 | } 85 | 86 | /** 87 | * {@inheritdoc} 88 | */ 89 | public function columns() 90 | { 91 | return collect($this->rawColumns())->map(function ($rawColumn) { 92 | return $this->mapRawColumnToObject($rawColumn); 93 | }); 94 | } 95 | 96 | /** 97 | * {@inheritdoc} 98 | */ 99 | public function indexes() 100 | { 101 | return collect($this->rawIndexes())->map(function ($rawIndex) { 102 | return $this->mapRawIndexToObject($rawIndex); 103 | }); 104 | } 105 | 106 | /** 107 | * {@inheritdoc} 108 | */ 109 | public function rows($table, Request $request) 110 | { 111 | $query = $this->buildSelectQuery($table, $request); 112 | 113 | $total = $query->count(); 114 | 115 | $query->when(! empty($request->sort_key), function ($q) use ($request) { 116 | return $q->orderBy( 117 | $request->sort_key, 118 | $request->input('sort_dir', 'asc') 119 | ); 120 | }); 121 | 122 | return [ 123 | 'total' => $total, 124 | 'data' => $query->offset($request->input('offset', 0))->limit($request->input('limit', 50))->get(), 125 | ]; 126 | } 127 | 128 | /** 129 | * Build select query. 130 | * 131 | * @param string $table 132 | * @return \Illuminate\Database\Query\Builder 133 | */ 134 | protected function buildSelectQuery($table, Request $request) 135 | { 136 | $query = $this->db->table($table); 137 | 138 | foreach ($request->input('filters', []) as $filter) { 139 | if ($filter['field'] == '__raw__') { 140 | if (! empty($filter['value'])) { 141 | $query->whereRaw($filter['value']); 142 | } 143 | } elseif ($filter['field'] == '__any__') { 144 | foreach ($this->columns($table)->pluck('column_name')->all() as $column) { 145 | $query->tap(function ($query) use ($column, $filter) { 146 | return $this->buildSubWhereClause($query, $column, $filter['operator'], $filter['value'], 'or'); 147 | }); 148 | } 149 | } else { 150 | $query->tap(function ($query) use ($filter) { 151 | return $this->buildSubWhereClause($query, $filter['field'], $filter['operator'], $filter['value']); 152 | }); 153 | } 154 | } 155 | 156 | return $query; 157 | } 158 | 159 | /** 160 | * @param \Illuminate\Database\Query\Builder $query 161 | * @param string $column 162 | * @param string $operator 163 | * @param string $value 164 | * @param string $boolean 165 | * @return \Illuminate\Database\Query\Builder 166 | */ 167 | public function buildSubWhereClause($query, $column, $operator = '=', $value = '', $boolean = 'and') 168 | { 169 | if (in_array($operator, ['IN', 'NOT IN'])) { 170 | return $query->whereIn($column, array_filter(explode(',', $value)), $boolean, $operator == 'NOT IN'); 171 | } 172 | 173 | if (in_array($operator, ['IS NULL', 'IS NOT NULL'])) { 174 | return $query->whereNull($column, $boolean, $operator == 'IS NOT NULL'); 175 | } 176 | 177 | if (in_array($operator, ['BETWEEN', 'NOT BETWEEN'])) { 178 | return $query->whereBetween($column, explode(' AND ', strtoupper($value)), $boolean, $operator == 'NOT BETWEEN'); 179 | } 180 | 181 | if (in_array($operator, ['LIKE', 'NOT LIKE'])) { 182 | return $query->where($column, $operator, '%'.$value.'%', $boolean); 183 | } 184 | 185 | return $query->where($column, $operator, $value, $boolean); 186 | } 187 | 188 | /** 189 | * Get list of raw tables. 190 | * 191 | * @return array 192 | */ 193 | abstract protected function rawTables(); 194 | 195 | /** 196 | * Map the raw table object to a Dibi Table instance. 197 | * 198 | * @param object $rawTable 199 | * @return \Cuonggt\Dibi\Table 200 | */ 201 | abstract protected function mapRawTableToObject($rawTable); 202 | 203 | /** 204 | * Get list of raw columns. 205 | * 206 | * @return array 207 | */ 208 | abstract protected function rawColumns(); 209 | 210 | /** 211 | * Map the raw column object to a Dibi TableColumn instance. 212 | * 213 | * @param object $rawColumn 214 | * @return \Cuonggt\Dibi\TableColumn 215 | */ 216 | abstract protected function mapRawColumnToObject($rawColumn); 217 | 218 | /** 219 | * Get list of raw indexes. 220 | * 221 | * @return array 222 | */ 223 | abstract protected function rawIndexes(); 224 | 225 | /** 226 | * Map the raw index object to a Dibi TableIndex instance. 227 | * 228 | * @param object $rawIndex 229 | * @return \Cuonggt\Dibi\TableIndex 230 | */ 231 | abstract protected function mapRawIndexToObject($rawIndex); 232 | } 233 | -------------------------------------------------------------------------------- /resources/js/components/TableStructure.vue: -------------------------------------------------------------------------------- 1 | 158 | 159 | 172 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | true, 8 | 'array_syntax' => ['syntax' => 'short'], 9 | 'binary_operator_spaces' => [ 10 | 'default' => 'single_space', 11 | ], 12 | 'blank_line_after_namespace' => true, 13 | 'blank_line_after_opening_tag' => true, 14 | 'blank_line_before_statement' => [ 15 | 'statements' => [ 16 | 'continue', 17 | 'return', 18 | ], 19 | ], 20 | 'no_blank_lines_before_namespace' => false, 21 | 'control_structure_braces' => true, 22 | 'control_structure_continuation_position' => [ 23 | 'position' => 'same_line', 24 | ], 25 | 'curly_braces_position' => [ 26 | 'control_structures_opening_brace' => 'same_line', 27 | 'functions_opening_brace' => 'next_line_unless_newline_at_signature_end', 28 | 'anonymous_functions_opening_brace' => 'same_line', 29 | 'classes_opening_brace' => 'next_line_unless_newline_at_signature_end', 30 | 'anonymous_classes_opening_brace' => 'next_line_unless_newline_at_signature_end', 31 | 'allow_single_line_empty_anonymous_classes' => false, 32 | 'allow_single_line_anonymous_functions' => false, 33 | ], 34 | 'cast_spaces' => true, 35 | 'class_attributes_separation' => [ 36 | 'elements' => [ 37 | 'const' => 'one', 38 | 'method' => 'one', 39 | 'property' => 'one', 40 | 'trait_import' => 'none', 41 | ], 42 | ], 43 | 'class_definition' => [ 44 | 'multi_line_extends_each_single_line' => true, 45 | 'single_item_single_line' => true, 46 | 'single_line' => true, 47 | ], 48 | 'clean_namespace' => true, 49 | 'compact_nullable_typehint' => true, 50 | 'concat_space' => [ 51 | 'spacing' => 'none', 52 | ], 53 | 'constant_case' => ['case' => 'lower'], 54 | 'declare_equal_normalize' => true, 55 | 'declare_parentheses' => true, 56 | 'elseif' => true, 57 | 'encoding' => true, 58 | 'full_opening_tag' => true, 59 | 'fully_qualified_strict_types' => true, 60 | 'function_declaration' => true, 61 | 'function_typehint_space' => true, 62 | 'general_phpdoc_tag_rename' => true, 63 | 'heredoc_to_nowdoc' => true, 64 | 'include' => true, 65 | 'increment_style' => ['style' => 'post'], 66 | 'indentation_type' => true, 67 | 'integer_literal_case' => true, 68 | 'lambda_not_used_import' => true, 69 | 'linebreak_after_opening_tag' => true, 70 | 'line_ending' => true, 71 | 'list_syntax' => true, 72 | 'lowercase_cast' => true, 73 | 'lowercase_keywords' => true, 74 | 'lowercase_static_reference' => true, 75 | 'magic_method_casing' => true, 76 | 'magic_constant_casing' => true, 77 | 'method_argument_space' => [ 78 | 'on_multiline' => 'ignore', 79 | ], 80 | 'method_chaining_indentation' => true, 81 | 'multiline_whitespace_before_semicolons' => [ 82 | 'strategy' => 'no_multi_line', 83 | ], 84 | 'native_function_casing' => true, 85 | 'native_function_type_declaration_casing' => true, 86 | 'no_alias_functions' => true, 87 | 'no_alias_language_construct_call' => true, 88 | 'no_alternative_syntax' => true, 89 | 'no_binary_string' => true, 90 | 'no_blank_lines_after_class_opening' => true, 91 | 'no_blank_lines_after_phpdoc' => true, 92 | 'no_closing_tag' => true, 93 | 'no_empty_phpdoc' => true, 94 | 'no_empty_statement' => true, 95 | 'no_extra_blank_lines' => [ 96 | 'tokens' => [ 97 | 'extra', 98 | 'throw', 99 | 'use', 100 | ], 101 | ], 102 | 'no_leading_import_slash' => true, 103 | 'no_leading_namespace_whitespace' => true, 104 | 'no_mixed_echo_print' => [ 105 | 'use' => 'echo', 106 | ], 107 | 'no_multiline_whitespace_around_double_arrow' => true, 108 | 'no_multiple_statements_per_line' => true, 109 | 'no_short_bool_cast' => true, 110 | 'no_singleline_whitespace_before_semicolons' => true, 111 | 'no_spaces_after_function_name' => true, 112 | 'no_space_around_double_colon' => true, 113 | 'no_spaces_around_offset' => [ 114 | 'positions' => ['inside', 'outside'], 115 | ], 116 | 'no_spaces_inside_parenthesis' => true, 117 | 'no_superfluous_phpdoc_tags' => [ 118 | 'allow_mixed' => true, 119 | 'allow_unused_params' => true, 120 | ], 121 | 'no_trailing_comma_in_singleline' => true, 122 | 'no_trailing_whitespace' => true, 123 | 'no_trailing_whitespace_in_comment' => true, 124 | 'no_unneeded_control_parentheses' => [ 125 | 'statements' => ['break', 'clone', 'continue', 'echo_print', 'return', 'switch_case', 'yield'], 126 | ], 127 | 'no_unneeded_curly_braces' => true, 128 | 'no_unset_cast' => true, 129 | 'no_unused_imports' => true, 130 | 'no_unreachable_default_argument_value' => true, 131 | 'no_useless_return' => true, 132 | 'no_whitespace_before_comma_in_array' => true, 133 | 'no_whitespace_in_blank_line' => true, 134 | 'normalize_index_brace' => true, 135 | 'not_operator_with_successor_space' => true, 136 | 'object_operator_without_whitespace' => true, 137 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 138 | 'psr_autoloading' => false, 139 | 'phpdoc_indent' => true, 140 | 'phpdoc_inline_tag_normalizer' => true, 141 | 'phpdoc_no_access' => true, 142 | 'phpdoc_no_package' => true, 143 | 'phpdoc_no_useless_inheritdoc' => true, 144 | 'phpdoc_order' => [ 145 | 'order' => ['param', 'return', 'throws'], 146 | ], 147 | 'phpdoc_scalar' => true, 148 | 'phpdoc_separation' => [ 149 | 'groups' => [ 150 | ['deprecated', 'link', 'see', 'since'], 151 | ['author', 'copyright', 'license'], 152 | ['category', 'package', 'subpackage'], 153 | ['property', 'property-read', 'property-write'], 154 | ['param', 'return'], 155 | ], 156 | ], 157 | 'phpdoc_single_line_var_spacing' => true, 158 | 'phpdoc_summary' => false, 159 | 'phpdoc_to_comment' => false, 160 | 'phpdoc_tag_type' => [ 161 | 'tags' => [ 162 | 'inheritdoc' => 'inline', 163 | ], 164 | ], 165 | 'phpdoc_trim' => true, 166 | 'phpdoc_types' => true, 167 | 'phpdoc_var_without_name' => true, 168 | 'return_type_declaration' => ['space_before' => 'none'], 169 | 'self_accessor' => false, 170 | 'self_static_accessor' => true, 171 | 'short_scalar_cast' => true, 172 | 'simplified_null_return' => false, 173 | 'single_blank_line_at_eof' => true, 174 | 'single_class_element_per_statement' => [ 175 | 'elements' => ['const', 'property'], 176 | ], 177 | 'single_import_per_statement' => true, 178 | 'single_line_after_imports' => true, 179 | 'single_line_comment_style' => [ 180 | 'comment_types' => ['hash'], 181 | ], 182 | 'single_quote' => true, 183 | 'single_space_around_construct' => true, 184 | 'space_after_semicolon' => true, 185 | 'standardize_not_equals' => true, 186 | 'statement_indentation' => true, 187 | 'switch_case_semicolon_to_colon' => true, 188 | 'switch_case_space' => true, 189 | 'ternary_operator_spaces' => true, 190 | 'trailing_comma_in_multiline' => ['elements' => ['arrays']], 191 | 'trim_array_spaces' => true, 192 | 'types_spaces' => true, 193 | 'unary_operator_spaces' => true, 194 | 'visibility_required' => [ 195 | 'elements' => ['method', 'property'], 196 | ], 197 | 'whitespace_after_comma_in_array' => true, 198 | ]; 199 | 200 | $finder = Finder::create() 201 | ->in([ 202 | __DIR__.'/src', 203 | __DIR__.'/tests', 204 | ]) 205 | ->name('*.php') 206 | ->notName([ 207 | '_ide_helper_actions.php', 208 | '_ide_helper_models.php', 209 | '_ide_helper.php', 210 | '.phpstorm.meta.php', 211 | '*.blade.php', 212 | ]) 213 | ->exclude([ 214 | 'storage', 215 | 'bootstrap/cache', 216 | 'node_modules', 217 | ]) 218 | ->ignoreDotFiles(true) 219 | ->ignoreVCS(true); 220 | 221 | return (new Config) 222 | ->setFinder($finder) 223 | ->setRules($rules) 224 | ->setRiskyAllowed(true) 225 | ->setUsingCache(true); 226 | -------------------------------------------------------------------------------- /public/app.css: -------------------------------------------------------------------------------- 1 | /*! tailwindcss v3.3.2 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid #e5e7eb;box-sizing:border-box}:after,:before{--tw-content:""}html{-webkit-text-size-adjust:100%;font-feature-settings:normal;font-family:Figtree,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-variation-settings:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}[hidden]{display:none}[multiple],[type=date],[type=datetime-local],[type=email],[type=month],[type=number],[type=password],[type=search],[type=tel],[type=text],[type=time],[type=url],[type=week],select,textarea{--tw-shadow:0 0 #0000;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-radius:0;border-width:1px;font-size:1rem;line-height:1.5rem;padding:.5rem .75rem}[multiple]:focus,[type=date]:focus,[type=datetime-local]:focus,[type=email]:focus,[type=month]:focus,[type=number]:focus,[type=password]:focus,[type=search]:focus,[type=tel]:focus,[type=text]:focus,[type=time]:focus,[type=url]:focus,[type=week]:focus,select:focus,textarea:focus{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);border-color:#2563eb;box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);outline:2px solid transparent;outline-offset:2px}input::-moz-placeholder,textarea::-moz-placeholder{color:#6b7280;opacity:1}input::placeholder,textarea::placeholder{color:#6b7280;opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em}::-webkit-datetime-edit,::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-meridiem-field,::-webkit-datetime-edit-millisecond-field,::-webkit-datetime-edit-minute-field,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-second-field,::-webkit-datetime-edit-year-field{padding-bottom:0;padding-top:0}select{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;-webkit-print-color-adjust:exact;print-color-adjust:exact}[multiple]{background-image:none;background-position:0 0;background-repeat:unset;background-size:initial;padding-right:.75rem;-webkit-print-color-adjust:unset;print-color-adjust:unset}[type=checkbox],[type=radio]{--tw-shadow:0 0 #0000;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;background-origin:border-box;border-color:#6b7280;border-width:1px;color:#2563eb;display:inline-block;flex-shrink:0;height:1rem;padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;-webkit-user-select:none;-moz-user-select:none;user-select:none;vertical-align:middle;width:1rem}[type=checkbox]{border-radius:0}[type=radio]{border-radius:100%}[type=checkbox]:focus,[type=radio]:focus{--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:2px;--tw-ring-offset-color:#fff;--tw-ring-color:#2563eb;--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);outline:2px solid transparent;outline-offset:2px}[type=checkbox]:checked,[type=radio]:checked{background-color:currentColor;background-position:50%;background-repeat:no-repeat;background-size:100% 100%;border-color:transparent}[type=checkbox]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 16 16' fill='%23fff' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M12.207 4.793a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L6.5 9.086l4.293-4.293a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E")}[type=radio]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg viewBox='0 0 16 16' fill='%23fff' xmlns='http://www.w3.org/2000/svg'%3E%3Ccircle cx='8' cy='8' r='3'/%3E%3C/svg%3E")}[type=checkbox]:checked:focus,[type=checkbox]:checked:hover,[type=radio]:checked:focus,[type=radio]:checked:hover{background-color:currentColor;border-color:transparent}[type=checkbox]:indeterminate{background-color:currentColor;background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3E%3Cpath stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3E%3C/svg%3E");background-position:50%;background-repeat:no-repeat;background-size:100% 100%;border-color:transparent}[type=checkbox]:indeterminate:focus,[type=checkbox]:indeterminate:hover{background-color:currentColor;border-color:transparent}[type=file]{background:unset;border-color:inherit;border-radius:0;border-width:0;font-size:unset;line-height:inherit;padding:0}[type=file]:focus{outline:1px solid ButtonText;outline:1px auto -webkit-focus-ring-color}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.fixed{position:fixed}.absolute{position:absolute}.sticky{position:sticky}.inset-0{inset:0}.bottom-0{bottom:0}.top-0{top:0}.mx-auto{margin-left:auto;margin-right:auto}.mb-6{margin-bottom:1.5rem}.ml-2{margin-left:.5rem}.mt-1{margin-top:.25rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.block{display:block}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.hidden{display:none}.h-0{height:0}.h-12{height:3rem}.h-16{height:4rem}.h-2{height:.5rem}.h-20{height:5rem}.h-3{height:.75rem}.h-4{height:1rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-full{height:100%}.h-screen{height:100vh}.w-12{width:3rem}.w-16{width:4rem}.w-2{width:.5rem}.w-20{width:5rem}.w-3{width:.75rem}.w-4{width:1rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-8{width:2rem}.w-full{width:100%}.min-w-full{min-width:100%}.max-w-xl{max-width:36rem}.flex-1{flex:1 1 0%}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.translate-y-0{--tw-translate-y:0px}.translate-y-0,.translate-y-4{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.translate-y-4{--tw-translate-y:1rem}.transform{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.cursor-pointer{cursor:pointer}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-bottom-width:calc(1px*var(--tw-divide-y-reverse));border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(229 231 235/var(--tw-divide-opacity))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.whitespace-nowrap{white-space:nowrap}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-l-full{border-bottom-left-radius:9999px;border-top-left-radius:9999px}.rounded-l-none{border-bottom-left-radius:0;border-top-left-radius:0}.rounded-r-none{border-bottom-right-radius:0;border-top-right-radius:0}.border{border-width:1px}.border-r-4{border-right-width:4px}.border-t-2{border-top-width:2px}.border-blue-500{--tw-border-opacity:1;border-color:rgb(59 130 246/var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-transparent{border-color:transparent}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-gray-500{--tw-bg-opacity:1;background-color:rgb(107 114 128/var(--tw-bg-opacity))}.bg-gray-700{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.p-2{padding:.5rem}.px-12{padding-left:3rem;padding-right:3rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-2{padding-bottom:.5rem;padding-top:.5rem}.py-3{padding-bottom:.75rem;padding-top:.75rem}.py-4{padding-bottom:1rem;padding-top:1rem}.py-6{padding-bottom:1.5rem;padding-top:1.5rem}.pl-4{padding-left:1rem}.pr-6{padding-right:1.5rem}.pr-8{padding-right:2rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.font-sans{font-family:Figtree,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-4{line-height:1rem}.leading-5{line-height:1.25rem}.tracking-wider{letter-spacing:.05em}.tracking-widest{letter-spacing:.1em}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.opacity-0{opacity:0}.opacity-100{opacity:1}.opacity-25{opacity:.25}.opacity-75{opacity:.75}.shadow-sm{--tw-shadow:0 1px 2px 0 rgba(0,0,0,.05);--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.shadow-sm,.shadow-xl{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);--tw-shadow-colored:0 20px 25px -5px var(--tw-shadow-color),0 8px 10px -6px var(--tw-shadow-color)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-duration:.15s;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1)}.duration-150,.transition-all{transition-duration:.15s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.ease-in{transition-timing-function:cubic-bezier(.4,0,1,1)}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.hover\:rounded-l-full:hover{border-bottom-left-radius:9999px;border-top-left-radius:9999px}.hover\:bg-gray-700:hover{--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}.hover\:text-gray-500:hover{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.focus\:border-blue-300:focus{--tw-border-opacity:1;border-color:rgb(147 197 253/var(--tw-border-opacity))}.focus\:border-gray-900:focus{--tw-border-opacity:1;border-color:rgb(17 24 39/var(--tw-border-opacity))}.focus\:border-indigo-300:focus{--tw-border-opacity:1;border-color:rgb(165 180 252/var(--tw-border-opacity))}.focus\:border-indigo-500:focus{--tw-border-opacity:1;border-color:rgb(99 102 241/var(--tw-border-opacity))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring:focus{--tw-ring-offset-shadow:var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow:var(--tw-ring-inset) 0 0 0 calc(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-indigo-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(199 210 254/var(--tw-ring-opacity))}.focus\:ring-indigo-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(99 102 241/var(--tw-ring-opacity))}.focus\:ring-opacity-50:focus{--tw-ring-opacity:0.5}.active\:bg-gray-50:active{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.active\:bg-gray-900:active{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}.active\:text-gray-800:active{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}.disabled\:opacity-25:disabled{opacity:.25}.disabled\:opacity-75:disabled{opacity:.75}@media (min-width:640px){.sm\:mx-auto{margin-left:auto;margin-right:auto}.sm\:w-full{width:100%}.sm\:max-w-2xl{max-width:42rem}.sm\:max-w-3xl{max-width:48rem}.sm\:max-w-4xl{max-width:56rem}.sm\:max-w-5xl{max-width:64rem}.sm\:max-w-6xl{max-width:72rem}.sm\:max-w-7xl{max-width:80rem}.sm\:max-w-lg{max-width:32rem}.sm\:max-w-md{max-width:28rem}.sm\:max-w-sm{max-width:24rem}.sm\:max-w-xl{max-width:36rem}.sm\:translate-y-0{--tw-translate-y:0px}.sm\:scale-100,.sm\:translate-y-0{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.sm\:scale-100{--tw-scale-x:1;--tw-scale-y:1}.sm\:scale-95{--tw-scale-x:.95;--tw-scale-y:.95;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.sm\:px-0{padding-left:0;padding-right:0}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}} 2 | -------------------------------------------------------------------------------- /resources/js/views/TableDetails.vue: -------------------------------------------------------------------------------- 1 | 276 | 277 | 440 | --------------------------------------------------------------------------------