├── .dockerignore ├── .env_example ├── .github └── workflows │ └── codeql-analysis.yml ├── .gitignore ├── README.md ├── assets ├── images │ ├── favicon │ │ ├── android-chrome-192x192.png │ │ ├── android-chrome-512x512.png │ │ ├── apple-touch-icon.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── mstile-150x150.png │ │ └── safari-pinned-tab.svg │ ├── loading.gif │ └── sandal-logo.png ├── js │ ├── app.js │ ├── components │ │ ├── LoadingBtn.vue │ │ ├── RunningQueries.vue │ │ ├── cluster.vue │ │ ├── node-list │ │ │ ├── index.vue │ │ │ ├── node.vue │ │ │ └── query.vue │ │ ├── notFound.vue │ │ └── sidebar.vue │ ├── pages │ │ └── App.vue │ ├── routes.js │ └── running-queries.js └── scss │ ├── app.scss │ ├── components │ ├── black-component.scss │ └── light-component.scss │ └── variables │ └── colors.scss ├── bin └── console ├── composer.json ├── composer.lock ├── config ├── bundles.php ├── packages │ ├── assets.yaml │ ├── cache.yaml │ ├── dev │ │ ├── debug.yaml │ │ ├── monolog.yaml │ │ └── web_profiler.yaml │ ├── framework.yaml │ ├── prod │ │ ├── deprecations.yaml │ │ ├── monolog.yaml │ │ ├── routing.yaml │ │ └── webpack_encore.yaml │ ├── routing.yaml │ ├── test │ │ ├── framework.yaml │ │ ├── monolog.yaml │ │ ├── twig.yaml │ │ ├── web_profiler.yaml │ │ └── webpack_encore.yaml │ ├── twig.yaml │ └── webpack_encore.yaml ├── routes.yaml ├── routes │ ├── annotations.yaml │ └── dev │ │ ├── framework.yaml │ │ └── web_profiler.yaml └── services.yaml ├── docker-compose.yaml ├── docker ├── dev │ ├── nginx │ │ ├── Dockerfile │ │ └── nginx │ │ │ └── conf.d │ │ │ └── default.conf │ └── php │ │ └── Dockerfile └── kubernetes │ ├── nginx │ └── Dockerfile │ └── php │ └── Dockerfile ├── package-lock.json ├── package.json ├── public ├── browserconfig.xml ├── index.php └── site.webmanifest ├── src ├── Controller │ ├── .gitignore │ ├── QueriesController.php │ ├── SPAQueriesController.php │ └── UserController.php ├── Kernel.php ├── Resolver │ └── ClusterNameResolver.php └── Service │ └── ClickHouseClient.php ├── symfony.lock ├── templates ├── base_spa.html.twig ├── rq.html.twig └── user_list.html.twig ├── webpack.config.js └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .gitignore 3 | .env.example 4 | .gitlab-ci* 5 | README.md 6 | docker 7 | docker-compose.yaml 8 | charts 9 | node_modules 10 | 11 | -------------------------------------------------------------------------------- /.env_example: -------------------------------------------------------------------------------- 1 | ###> symfony/framework-bundle ### 2 | APP_ENV=prod 3 | APP_SECRET=84b2ebbee88b31f727a935432d415bbd 4 | #TRUSTED_PROXIES=127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 5 | #TRUSTED_HOSTS='^(localhost|example\.com)$' 6 | ###< symfony/framework-bundle ### 7 | 8 | CLICKHOUSE_HOST= 9 | CLICKHOUSE_PORT= 10 | CLICKHOUSE_USERNAME= 11 | CLICKHOUSE_PASSWORD= 12 | CLICKHOUSE_DB_NAME= 13 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '27 20 * * 4' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'javascript' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | ###> symfony/framework-bundle ### 4 | /.env.local 5 | /.env.local.php 6 | /.env.*.local 7 | /.env 8 | /config/secrets/prod/prod.decrypt.private.php 9 | /public/bundles/ 10 | /src/.preload.php 11 | /var/ 12 | /vendor/ 13 | ###< symfony/framework-bundle ### 14 | 15 | ###> symfony/webpack-encore-bundle ### 16 | /node_modules/ 17 | /public/build/ 18 | npm-debug.log 19 | yarn-error.log 20 | ###< symfony/webpack-encore-bundle ### 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chadmin 2 | Chadmin is easy to use ClickHouse running query dashboard. 3 | 4 | You can watch your currently running queries for the entire ClickHouse cluster and kill them if you want. 5 | 6 | How to install: 7 | 8 | 0. `cp .env_example .env` and fill ClickHouse credentials 9 | 1. `composer install` 10 | 2. `yarn install` 11 | 3. `yarn build` 12 | 4. `docker-compose build` 13 | 5. `docker-compose up -d` 14 | 15 | 16 | -------------------------------------------------------------------------------- /assets/images/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bun4uk/chadmin/d6bbb9f9f7d3654102ad70f7368d046f3832b6ed/assets/images/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /assets/images/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bun4uk/chadmin/d6bbb9f9f7d3654102ad70f7368d046f3832b6ed/assets/images/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /assets/images/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bun4uk/chadmin/d6bbb9f9f7d3654102ad70f7368d046f3832b6ed/assets/images/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /assets/images/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bun4uk/chadmin/d6bbb9f9f7d3654102ad70f7368d046f3832b6ed/assets/images/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /assets/images/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bun4uk/chadmin/d6bbb9f9f7d3654102ad70f7368d046f3832b6ed/assets/images/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /assets/images/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bun4uk/chadmin/d6bbb9f9f7d3654102ad70f7368d046f3832b6ed/assets/images/favicon/favicon.ico -------------------------------------------------------------------------------- /assets/images/favicon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bun4uk/chadmin/d6bbb9f9f7d3654102ad70f7368d046f3832b6ed/assets/images/favicon/mstile-150x150.png -------------------------------------------------------------------------------- /assets/images/favicon/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /assets/images/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bun4uk/chadmin/d6bbb9f9f7d3654102ad70f7368d046f3832b6ed/assets/images/loading.gif -------------------------------------------------------------------------------- /assets/images/sandal-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bun4uk/chadmin/d6bbb9f9f7d3654102ad70f7368d046f3832b6ed/assets/images/sandal-logo.png -------------------------------------------------------------------------------- /assets/js/app.js: -------------------------------------------------------------------------------- 1 | import '../scss/app.scss'; 2 | import Vue from 'vue'; 3 | import PrettyCheckbox from 'pretty-checkbox-vue'; 4 | import Routes from './routes.js'; 5 | import App from './pages/App'; 6 | import Clipboard from 'v-clipboard' 7 | 8 | Vue.use(Clipboard) 9 | Vue.use(PrettyCheckbox); 10 | 11 | const app = new Vue({ 12 | el: '#app', 13 | router: Routes, 14 | render: h => h(App), 15 | }).$mount('#app'); 16 | 17 | export default app; -------------------------------------------------------------------------------- /assets/js/components/LoadingBtn.vue: -------------------------------------------------------------------------------- 1 | 2 | 11 | Submit 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 38 | 39 | 171 | -------------------------------------------------------------------------------- /assets/js/components/RunningQueries.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 71 | -------------------------------------------------------------------------------- /assets/js/components/cluster.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ this.cluster }} Queries 8 | 9 | 10 | 11 | 12 | 20 | 🔄 21 | 22 | 23 | 24 | Auto-refresh 33 | 34 | 35 | 36 | refresh interval 37 | 38 | 5 sec 39 | 10 sec 40 | 30 sec 41 | 1 min 42 | 5 min 43 | 10 min 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 123 | 124 | -------------------------------------------------------------------------------- /assets/js/components/node-list/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 11 | 12 | 13 | 14 | 15 | 32 | -------------------------------------------------------------------------------- /assets/js/components/node-list/node.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Node: {{ nodeIp }} 7 | 8 | 9 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 45 | 46 | 68 | -------------------------------------------------------------------------------- /assets/js/components/node-list/query.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | {{ query.user }} 9 | 10 | 11 | {{ query.elapsed.toFixed(2) | formattedTime }} 12 | 13 | 14 | 15 | 20 | 21 | {{ query.formatted_memory_usage }} 22 | 23 | 28 | 34 | {{ copy_query_sql_text }} 35 | 36 | 43 | {{ copy_query_id_text }} 44 | 45 | {{ query.query_id }} 46 | 47 | 48 | 49 | Killing... 50 | 51 | 58 | X 59 | 60 | 61 | Killing... 62 | 63 | 64 | 65 | 66 | 67 | 68 | 143 | 144 | 215 | -------------------------------------------------------------------------------- /assets/js/components/notFound.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 404 Page not found 7 | 8 | 9 | its just another vue route - you can change the url above to any thing 10 | 11 | 12 | 13 | 14 | 15 | 16 | 21 | 22 | -------------------------------------------------------------------------------- /assets/js/components/sidebar.vue: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | Clusters 10 | 11 | 12 | 13 | 18 | 24 | {{ cluster.name }} 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 38 | 39 | 40 | 41 | 80 | 81 | 99 | 100 | -------------------------------------------------------------------------------- /assets/js/pages/App.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | -------------------------------------------------------------------------------- /assets/js/routes.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueRouter from 'vue-router'; 3 | 4 | import RunningQueries from "@/components/RunningQueries"; 5 | 6 | Vue.use(VueRouter); 7 | 8 | const router = new VueRouter({ 9 | mode: 'history', 10 | routes: [ 11 | { 12 | path: '/', 13 | name: 'rq', 14 | component: RunningQueries 15 | } 16 | ] 17 | }); 18 | 19 | export default router; -------------------------------------------------------------------------------- /assets/js/running-queries.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import App from '@/components/RunningQueries'; 3 | 4 | new Vue({ 5 | render: (h) => h(App), 6 | }).$mount('#app'); 7 | 8 | -------------------------------------------------------------------------------- /assets/scss/app.scss: -------------------------------------------------------------------------------- 1 | @import './components/black-component'; 2 | @import './components/light-component'; 3 | @import './variables/colors'; 4 | @import '~bootstrap'; 5 | @import url('https://fonts.googleapis.com/css2?family=Montserrat:ital,wght@0,400;0,500;0,700;1,400;1,500;1,700&display=swap'); 6 | @import '~pretty-checkbox/src/pretty-checkbox.scss'; 7 | 8 | 9 | html { 10 | min-height: 100%; 11 | } 12 | body { 13 | font-family: 'Montserrat', sans-serif; 14 | a, a:hover { 15 | color: #000; 16 | } 17 | } 18 | 19 | .header { 20 | @include black-component; 21 | 22 | margin-bottom: 30px; 23 | border-bottom: 2px solid $red-component-border; 24 | padding: 0; 25 | 26 | .logo { 27 | width: 50px; 28 | height: auto; 29 | } 30 | } 31 | 32 | .footer { 33 | @include black-component; 34 | 35 | border-top: 2px solid $red-component-border; 36 | color: #fff; 37 | height: 90px; 38 | 39 | span { 40 | color: #ff0000; 41 | } 42 | 43 | a { 44 | color: #fff; 45 | text-decoration: underline; 46 | } 47 | 48 | a:hover { 49 | color: #efefee; 50 | } 51 | } 52 | 53 | .component-light { 54 | @include light-component; 55 | margin-top: 25px; 56 | padding: 20px; 57 | display: flex; 58 | flex-direction: column; 59 | width: 900px; 60 | } 61 | 62 | .aside-collapsed { 63 | padding: 0 15px; 64 | } 65 | 66 | /* Анимации появления и исчезновения могут иметь */ 67 | /* различные продолжительности и динамику. */ 68 | .slide-fade-enter-active { 69 | transition: all .3s ease; 70 | } 71 | .slide-fade-leave-active { 72 | transition: all .8s cubic-bezier(1.0, 0.5, 0.8, 1.0); 73 | } 74 | .slide-fade-enter, .slide-fade-leave-to 75 | /* .slide-fade-leave-active до версии 2.1.8 */ { 76 | transform: translateX(10px); 77 | opacity: 0; 78 | } -------------------------------------------------------------------------------- /assets/scss/components/black-component.scss: -------------------------------------------------------------------------------- 1 | @import '../variables/colors'; 2 | 3 | @mixin black-component { 4 | background-color: $black-component-background; 5 | padding: 30px; 6 | } 7 | -------------------------------------------------------------------------------- /assets/scss/components/light-component.scss: -------------------------------------------------------------------------------- 1 | @import '../variables/colors'; 2 | 3 | @mixin light-component { 4 | border: 1px solid $light-component-border; 5 | box-shadow: 0px 0px 7px 4px #efefee; 6 | border-radius: 5px; 7 | } 8 | -------------------------------------------------------------------------------- /assets/scss/variables/colors.scss: -------------------------------------------------------------------------------- 1 | 2 | $light-component-border: #efefee; 3 | $light-component-link: #007bff; 4 | $blue-component-link-hover: #add8e6; 5 | 6 | $black-component-background: #343a40; 7 | $red-component-border: #BA1414; 8 | 9 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | getParameterOption(['--env', '-e'], null, true)) { 24 | putenv('APP_ENV='.$_SERVER['APP_ENV'] = $_ENV['APP_ENV'] = $env); 25 | } 26 | 27 | if ($input->hasParameterOption('--no-debug', true)) { 28 | putenv('APP_DEBUG='.$_SERVER['APP_DEBUG'] = $_ENV['APP_DEBUG'] = '0'); 29 | } 30 | 31 | (new Dotenv())->bootEnv(dirname(__DIR__).'/.env'); 32 | 33 | if ($_SERVER['APP_DEBUG']) { 34 | umask(0000); 35 | 36 | if (class_exists(Debug::class)) { 37 | Debug::enable(); 38 | } 39 | } 40 | 41 | $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); 42 | $application = new Application($kernel); 43 | $application->run($input); 44 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "project", 3 | "license": "proprietary", 4 | "require": { 5 | "php": "^8.0", 6 | "ext-ctype": "*", 7 | "ext-curl": "*", 8 | "ext-iconv": "*", 9 | "ext-json": "*", 10 | "ext-pdo": "*", 11 | "doctrine/annotations": "1.*", 12 | "smi2/phpclickhouse": "1.*", 13 | "symfony/console": "5.*", 14 | "symfony/dotenv": "5.*", 15 | "symfony/flex": "1.*", 16 | "symfony/framework-bundle": "5.*", 17 | "symfony/http-client": "5.*", 18 | "symfony/http-foundation": "5.*", 19 | "symfony/twig-bundle": "5.*", 20 | "symfony/var-dumper": "5.*", 21 | "symfony/webpack-encore-bundle": "^v1.11", 22 | "symfony/yaml": "5.*", 23 | "twig/extra-bundle": "^2.12|^3.0", 24 | "twig/twig": "^2.12|^3.0" 25 | }, 26 | "require-dev": { 27 | "roave/security-advisories": "dev-master", 28 | "symfony/debug-bundle": "^5.1", 29 | "symfony/maker-bundle": "^1.2", 30 | "symfony/monolog-bundle": "^3.6", 31 | "symfony/stopwatch": "^5.1", 32 | "symfony/web-profiler-bundle": "^5.1" 33 | }, 34 | "config": { 35 | "preferred-install": { 36 | "*": "dist" 37 | }, 38 | "sort-packages": true 39 | }, 40 | "autoload": { 41 | "psr-4": { 42 | "App\\": "src/" 43 | } 44 | }, 45 | "autoload-dev": { 46 | "psr-4": { 47 | "App\\Tests\\": "tests/" 48 | } 49 | }, 50 | "replace": { 51 | "paragonie/random_compat": "2.*", 52 | "symfony/polyfill-ctype": "*", 53 | "symfony/polyfill-iconv": "*", 54 | "symfony/polyfill-php72": "*", 55 | "symfony/polyfill-php71": "*", 56 | "symfony/polyfill-php70": "*", 57 | "symfony/polyfill-php56": "*" 58 | }, 59 | "scripts": { 60 | "auto-scripts": { 61 | "cache:clear": "symfony-cmd", 62 | "assets:install %PUBLIC_DIR%": "symfony-cmd" 63 | }, 64 | "post-install-cmd": [ 65 | "@auto-scripts" 66 | ], 67 | "post-update-cmd": [ 68 | "@auto-scripts" 69 | ] 70 | }, 71 | "conflict": { 72 | "symfony/symfony": "*" 73 | }, 74 | "extra": { 75 | "symfony": { 76 | "allow-contrib": false, 77 | "require": "5.*" 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /config/bundles.php: -------------------------------------------------------------------------------- 1 | ['all' => true], 5 | Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true], 6 | Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true], 7 | Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true], 8 | Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true], 9 | Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true], 10 | Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true], 11 | Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true], 12 | ]; 13 | -------------------------------------------------------------------------------- /config/packages/assets.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | assets: 3 | json_manifest_path: '%kernel.project_dir%/public/build/manifest.json' 4 | -------------------------------------------------------------------------------- /config/packages/cache.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | cache: 3 | # Unique name of your app: used to compute stable namespaces for cache keys. 4 | #prefix_seed: your_vendor_name/app_name 5 | 6 | # The "app" cache stores to the filesystem by default. 7 | # The data in this cache should persist between deploys. 8 | # Other options include: 9 | 10 | # Redis 11 | #app: cache.adapter.redis 12 | #default_redis_provider: redis://localhost 13 | 14 | # APCu (not recommended with heavy random-write workloads as memory fragmentation can cause perf issues) 15 | #app: cache.adapter.apcu 16 | 17 | # Namespaced pools use the above "app" backend by default 18 | #pools: 19 | #my.dedicated.cache: null 20 | -------------------------------------------------------------------------------- /config/packages/dev/debug.yaml: -------------------------------------------------------------------------------- 1 | debug: 2 | # Forwards VarDumper Data clones to a centralized server allowing to inspect dumps on CLI or in your browser. 3 | # See the "server:dump" command to start a new server. 4 | dump_destination: "tcp://%env(VAR_DUMPER_SERVER)%" 5 | -------------------------------------------------------------------------------- /config/packages/dev/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | main: 4 | type: stream 5 | path: "%kernel.logs_dir%/%kernel.environment%.log" 6 | level: debug 7 | channels: ["!event"] 8 | # uncomment to get logging in your browser 9 | # you may have to allow bigger header sizes in your Web server configuration 10 | #firephp: 11 | # type: firephp 12 | # level: info 13 | #chromephp: 14 | # type: chromephp 15 | # level: info 16 | console: 17 | type: console 18 | process_psr_3_messages: false 19 | channels: ["!event", "!doctrine", "!console"] 20 | -------------------------------------------------------------------------------- /config/packages/dev/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | web_profiler: 2 | toolbar: true 3 | intercept_redirects: false 4 | 5 | framework: 6 | profiler: { only_exceptions: false } 7 | -------------------------------------------------------------------------------- /config/packages/framework.yaml: -------------------------------------------------------------------------------- 1 | # see https://symfony.com/doc/current/reference/configuration/framework.html 2 | framework: 3 | secret: '%env(APP_SECRET)%' 4 | #csrf_protection: true 5 | #http_method_override: true 6 | 7 | # Enables session support. Note that the session will ONLY be started if you read or write from it. 8 | # Remove or comment this section to explicitly disable session support. 9 | session: 10 | handler_id: null 11 | cookie_secure: auto 12 | cookie_samesite: lax 13 | 14 | #esi: true 15 | #fragments: true 16 | php_errors: 17 | log: true 18 | -------------------------------------------------------------------------------- /config/packages/prod/deprecations.yaml: -------------------------------------------------------------------------------- 1 | # As of Symfony 5.1, deprecations are logged in the dedicated "deprecation" channel when it exists 2 | #monolog: 3 | # channels: [deprecation] 4 | # handlers: 5 | # deprecation: 6 | # type: stream 7 | # channels: [deprecation] 8 | # path: "%kernel.logs_dir%/%kernel.environment%.deprecations.log" 9 | -------------------------------------------------------------------------------- /config/packages/prod/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | main: 4 | type: fingers_crossed 5 | action_level: error 6 | handler: nested 7 | excluded_http_codes: [404, 405] 8 | buffer_size: 50 # How many messages should be saved? Prevent memory leaks 9 | nested: 10 | type: stream 11 | path: "%kernel.logs_dir%/%kernel.environment%.log" 12 | level: debug 13 | console: 14 | type: console 15 | process_psr_3_messages: false 16 | channels: ["!event", "!doctrine"] 17 | -------------------------------------------------------------------------------- /config/packages/prod/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | strict_requirements: null 4 | -------------------------------------------------------------------------------- /config/packages/prod/webpack_encore.yaml: -------------------------------------------------------------------------------- 1 | #webpack_encore: 2 | # Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes) 3 | # Available in version 1.2 4 | #cache: true 5 | -------------------------------------------------------------------------------- /config/packages/routing.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | router: 3 | utf8: true 4 | 5 | # Configure how to generate URLs in non-HTTP contexts, such as CLI commands. 6 | # See https://symfony.com/doc/current/routing.html#generating-urls-in-commands 7 | #default_uri: http://localhost 8 | -------------------------------------------------------------------------------- /config/packages/test/framework.yaml: -------------------------------------------------------------------------------- 1 | framework: 2 | test: true 3 | session: 4 | storage_id: session.storage.mock_file 5 | -------------------------------------------------------------------------------- /config/packages/test/monolog.yaml: -------------------------------------------------------------------------------- 1 | monolog: 2 | handlers: 3 | main: 4 | type: fingers_crossed 5 | action_level: error 6 | handler: nested 7 | excluded_http_codes: [404, 405] 8 | channels: ["!event"] 9 | nested: 10 | type: stream 11 | path: "%kernel.logs_dir%/%kernel.environment%.log" 12 | level: debug 13 | -------------------------------------------------------------------------------- /config/packages/test/twig.yaml: -------------------------------------------------------------------------------- 1 | twig: 2 | strict_variables: true 3 | -------------------------------------------------------------------------------- /config/packages/test/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | web_profiler: 2 | toolbar: false 3 | intercept_redirects: false 4 | 5 | framework: 6 | profiler: { collect: false } 7 | -------------------------------------------------------------------------------- /config/packages/test/webpack_encore.yaml: -------------------------------------------------------------------------------- 1 | #webpack_encore: 2 | # strict_mode: false 3 | -------------------------------------------------------------------------------- /config/packages/twig.yaml: -------------------------------------------------------------------------------- 1 | twig: 2 | default_path: '%kernel.project_dir%/templates' 3 | -------------------------------------------------------------------------------- /config/packages/webpack_encore.yaml: -------------------------------------------------------------------------------- 1 | webpack_encore: 2 | # The path where Encore is building the assets - i.e. Encore.setOutputPath() 3 | output_path: '%kernel.project_dir%/public/build' 4 | # If multiple builds are defined (as shown below), you can disable the default build: 5 | # output_path: false 6 | 7 | # if using Encore.enableIntegrityHashes() and need the crossorigin attribute (default: false, or use 'anonymous' or 'use-credentials') 8 | # crossorigin: 'anonymous' 9 | 10 | # preload all rendered script and link tags automatically via the http2 Link header 11 | # preload: true 12 | 13 | # Throw an exception if the entrypoints.json file is missing or an entry is missing from the data 14 | # strict_mode: false 15 | 16 | # if you have multiple builds: 17 | # builds: 18 | # pass "frontend" as the 3rg arg to the Twig functions 19 | # {{ encore_entry_script_tags('entry1', null, 'frontend') }} 20 | 21 | # frontend: '%kernel.project_dir%/public/frontend/build' 22 | 23 | # Cache the entrypoints.json (rebuild Symfony's cache when entrypoints.json changes) 24 | # Put in config/packages/prod/webpack_encore.yaml 25 | # cache: true 26 | -------------------------------------------------------------------------------- /config/routes.yaml: -------------------------------------------------------------------------------- 1 | #index: 2 | # path: / 3 | # controller: App\Controller\DefaultController::index 4 | -------------------------------------------------------------------------------- /config/routes/annotations.yaml: -------------------------------------------------------------------------------- 1 | controllers: 2 | resource: ../../src/Controller/ 3 | type: annotation 4 | 5 | kernel: 6 | resource: ../../src/Kernel.php 7 | type: annotation 8 | -------------------------------------------------------------------------------- /config/routes/dev/framework.yaml: -------------------------------------------------------------------------------- 1 | _errors: 2 | resource: '@FrameworkBundle/Resources/config/routing/errors.xml' 3 | prefix: /_error 4 | -------------------------------------------------------------------------------- /config/routes/dev/web_profiler.yaml: -------------------------------------------------------------------------------- 1 | web_profiler_wdt: 2 | resource: '@WebProfilerBundle/Resources/config/routing/wdt.xml' 3 | prefix: /_wdt 4 | 5 | web_profiler_profiler: 6 | resource: '@WebProfilerBundle/Resources/config/routing/profiler.xml' 7 | prefix: /_profiler 8 | -------------------------------------------------------------------------------- /config/services.yaml: -------------------------------------------------------------------------------- 1 | # This file is the entry point to configure your own services. 2 | # Files in the packages/ subdirectory configure your dependencies. 3 | 4 | # Put parameters here that don't need to change on each machine where the app is deployed 5 | # https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration 6 | parameters: 7 | 8 | services: 9 | # default configuration for services in *this* file 10 | _defaults: 11 | autowire: true # Automatically injects dependencies in your services. 12 | autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. 13 | 14 | # makes classes in src/ available to be used as services 15 | # this creates a service per class whose id is the fully-qualified class name 16 | App\: 17 | resource: '../src/*' 18 | exclude: '../src/{DependencyInjection,Entity,Migrations,Tests,Kernel.php}' 19 | 20 | # controllers are imported separately to make sure services can be injected 21 | # as action arguments even if you don't extend any base controller class 22 | App\Controller\: 23 | resource: '../src/Controller' 24 | tags: ['controller.service_arguments'] 25 | 26 | # add more service definitions when explicit configuration is needed 27 | # please note that last definitions always *replace* previous ones 28 | 29 | App\Service\ClickHouseClient: 30 | public: true 31 | arguments: 32 | - '%env(CLICKHOUSE_HOST)%' 33 | - '%env(CLICKHOUSE_PORT)%' 34 | - '%env(CLICKHOUSE_USERNAME)%' 35 | - '%env(CLICKHOUSE_PASSWORD)%' 36 | - '%env(CLICKHOUSE_DB_NAME)%' -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | nginx: 4 | build: 5 | context: . 6 | dockerfile: docker/dev/nginx/Dockerfile 7 | image: dev/nginx-chadmin:1.18.0 8 | volumes: 9 | - ./docker/dev/nginx/nginx/conf.d/default.conf:/etc/nginx/conf.d/default.conf 10 | - .:/var/www/html 11 | ports: 12 | - "80:80" 13 | env_file: 14 | - .env 15 | networks: 16 | - sp-net 17 | 18 | app: 19 | build: 20 | context: . 21 | dockerfile: docker/dev/php/Dockerfile 22 | image: dev/php-chadmin:8.0.3 23 | volumes: 24 | - .:/var/www/html 25 | env_file: 26 | - .env 27 | networks: 28 | - sp-net 29 | 30 | networks: 31 | sp-net: 32 | driver: "bridge" 33 | -------------------------------------------------------------------------------- /docker/dev/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx:1.18.0-alpine AS builder 2 | 3 | ENV VTS_VERSION 0.1.18 4 | ENV GEOIP2_VERSION 3.3 5 | 6 | # Download sources 7 | RUN wget "http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz" -O nginx.tar.gz && \ 8 | wget "https://github.com/vozlt/nginx-module-vts/archive/v${VTS_VERSION}.tar.gz" -O vts.tar.gz && \ 9 | wget "https://github.com/leev/ngx_http_geoip2_module/archive/${GEOIP2_VERSION}.tar.gz" -O geoip2.tar.gz 10 | 11 | # For latest build deps, see https://github.com/nginxinc/docker-nginx/blob/master/mainline/alpine/Dockerfile 12 | RUN apk add --no-cache --virtual .build-deps \ 13 | gcc \ 14 | libc-dev \ 15 | make \ 16 | openssl-dev \ 17 | pcre-dev \ 18 | zlib-dev \ 19 | linux-headers \ 20 | curl \ 21 | gnupg \ 22 | libxslt-dev \ 23 | gd-dev \ 24 | geoip-dev \ 25 | libmaxminddb-dev \ 26 | g++ \ 27 | bash 28 | 29 | # Reuse same cli arguments as the nginx:alpine image used to build 30 | RUN CONFARGS=$(nginx -V 2>&1 | sed -n -e 's/^.*arguments: //p') \ 31 | mkdir -p /usr/src && \ 32 | tar -zxC /usr/src -f nginx.tar.gz && \ 33 | tar -xzvf "vts.tar.gz" && \ 34 | tar -xzvf "geoip2.tar.gz" && \ 35 | VTSDIR="$(pwd)/nginx-module-vts-${VTS_VERSION}" && \ 36 | GEOIP2="$(pwd)/ngx_http_geoip2_module-${GEOIP2_VERSION}" && \ 37 | cd /usr/src/nginx-$NGINX_VERSION && \ 38 | ./configure --with-compat --with-stream $CONFARGS --add-dynamic-module=$VTSDIR --add-dynamic-module=$GEOIP2 && \ 39 | make modules 40 | 41 | FROM nginx:1.18.0-alpine 42 | 43 | # Copy all modules 44 | COPY --from=builder /usr/src/nginx-$NGINX_VERSION/objs/*.so /usr/lib/nginx/modules/ 45 | RUN apk add --no-cache --virtual .build-deps \ 46 | vim \ 47 | libmaxminddb-dev \ 48 | libstdc++ 49 | 50 | WORKDIR /var/www/html 51 | 52 | STOPSIGNAL SIGQUIT 53 | -------------------------------------------------------------------------------- /docker/dev/nginx/nginx/conf.d/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name _; 4 | root /var/www/html/public; 5 | index index.php; 6 | 7 | location / { 8 | # try to serve file directly, fallback to index.php 9 | try_files $uri /index.php$is_args$args; 10 | } 11 | 12 | location ~* \.(php) { 13 | fastcgi_pass app:9000; 14 | fastcgi_split_path_info ^(.+\.php)(/.*)$; 15 | include fastcgi_params; 16 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 17 | fastcgi_param DOCUMENT_ROOT $document_root; 18 | } 19 | 20 | location ~ \.php$ { 21 | return 404; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /docker/dev/php/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.0.3-fpm-alpine 2 | 3 | RUN apk add --no-cache \ 4 | \ 5 | libzip-dev \ 6 | \ 7 | icu-dev \ 8 | libxml2-dev \ 9 | \ 10 | mysql-client \ 11 | tzdata \ 12 | git \ 13 | bash 14 | 15 | RUN docker-php-ext-install sockets intl bcmath zip pcntl pdo_mysql mysqli soap \ 16 | && docker-php-ext-enable opcache 17 | 18 | RUN php -r "readfile('http://getcomposer.org/installer');" | php -- --install-dir=/usr/bin/ --version=2.0.8 --filename=composer 19 | 20 | WORKDIR /var/www/html 21 | 22 | STOPSIGNAL SIGQUIT 23 | 24 | EXPOSE 9000 25 | 26 | CMD ["php-fpm", "-R", "-F"] 27 | -------------------------------------------------------------------------------- /docker/kubernetes/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:15.4 as frontend 2 | 3 | RUN mkdir -p /app/public 4 | 5 | COPY package.json webpack.config.js yarn.lock /app/ 6 | COPY assets/ /app/assets/ 7 | 8 | WORKDIR /app 9 | 10 | RUN yarn install && yarn build 11 | # 12 | FROM nginx:1.18.0-alpine AS builder 13 | 14 | ENV VTS_VERSION 0.1.18 15 | ENV GEOIP2_VERSION 3.3 16 | 17 | # Download sources 18 | RUN wget "http://nginx.org/download/nginx-${NGINX_VERSION}.tar.gz" -O nginx.tar.gz && \ 19 | wget "https://github.com/vozlt/nginx-module-vts/archive/v${VTS_VERSION}.tar.gz" -O vts.tar.gz && \ 20 | wget "https://github.com/leev/ngx_http_geoip2_module/archive/${GEOIP2_VERSION}.tar.gz" -O geoip2.tar.gz 21 | 22 | # For latest build deps, see https://github.com/nginxinc/docker-nginx/blob/master/mainline/alpine/Dockerfile 23 | RUN apk add --no-cache --virtual .build-deps \ 24 | gcc \ 25 | libc-dev \ 26 | make \ 27 | openssl-dev \ 28 | pcre-dev \ 29 | zlib-dev \ 30 | linux-headers \ 31 | curl \ 32 | gnupg \ 33 | libxslt-dev \ 34 | gd-dev \ 35 | geoip-dev \ 36 | libmaxminddb-dev \ 37 | g++ \ 38 | bash 39 | 40 | # Reuse same cli arguments as the nginx:alpine image used to build 41 | RUN CONFARGS=$(nginx -V 2>&1 | sed -n -e 's/^.*arguments: //p') \ 42 | mkdir -p /usr/src && \ 43 | tar -zxC /usr/src -f nginx.tar.gz && \ 44 | tar -xzvf "vts.tar.gz" && \ 45 | tar -xzvf "geoip2.tar.gz" && \ 46 | VTSDIR="$(pwd)/nginx-module-vts-${VTS_VERSION}" && \ 47 | GEOIP2="$(pwd)/ngx_http_geoip2_module-${GEOIP2_VERSION}" && \ 48 | cd /usr/src/nginx-$NGINX_VERSION && \ 49 | ./configure --with-compat --with-stream $CONFARGS --add-dynamic-module=$VTSDIR --add-dynamic-module=$GEOIP2 && \ 50 | make modules 51 | 52 | FROM nginx:1.18.0-alpine 53 | 54 | # Copy all modules 55 | COPY --from=builder /usr/src/nginx-$NGINX_VERSION/objs/*.so /usr/lib/nginx/modules/ 56 | RUN apk add --no-cache --virtual .build-deps \ 57 | vim \ 58 | libmaxminddb-dev \ 59 | libstdc++ 60 | 61 | WORKDIR /var/www/html/ 62 | 63 | STOPSIGNAL SIGQUIT 64 | 65 | COPY --from=frontend /app/public/ /var/www/html/public/ 66 | COPY --from=frontend /app/webpack.config.js /var/www/html/webpack.config.js 67 | COPY . /var/www/html 68 | -------------------------------------------------------------------------------- /docker/kubernetes/php/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM composer:2.0.8 as vendor 2 | 3 | COPY composer.json composer.json 4 | COPY composer.lock composer.lock 5 | 6 | RUN composer install 7 | # 8 | FROM node:15.4 as frontend 9 | 10 | RUN mkdir -p /app/public 11 | 12 | COPY package.json webpack.config.js yarn.lock /app/ 13 | COPY assets/ /app/assets/ 14 | 15 | WORKDIR /app 16 | 17 | RUN yarn install && yarn build 18 | # 19 | FROM php:8.0.3-fpm-alpine 20 | 21 | RUN apk add --no-cache \ 22 | \ 23 | libzip-dev \ 24 | \ 25 | icu-dev \ 26 | libxml2-dev \ 27 | \ 28 | mysql-client \ 29 | tzdata \ 30 | git \ 31 | bash 32 | 33 | RUN docker-php-ext-install sockets intl bcmath zip pcntl pdo_mysql mysqli soap \ 34 | && docker-php-ext-enable opcache 35 | 36 | WORKDIR /var/www/html 37 | 38 | STOPSIGNAL SIGQUIT 39 | 40 | EXPOSE 9000 41 | 42 | CMD ["php-fpm", "-R", "-F"] 43 | 44 | COPY --from=vendor /app/vendor/ /var/www/html/vendor/ 45 | COPY --from=frontend /app/public/ /var/www/html/public/ 46 | COPY --from=frontend /app/webpack.config.js /var/www/html/webpack.config.js 47 | COPY . /var/www/html 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "browserslist": [ 3 | "defaults" 4 | ], 5 | "devDependencies": { 6 | "@symfony/webpack-encore": "^1.1.2", 7 | "@vue/compiler-sfc": "^3.0.7", 8 | "axios": "^0.21.1", 9 | "bootstrap": "latest", 10 | "core-js": "^3.9.1", 11 | "eslint": "^7.22.0", 12 | "eslint-config-airbnb-base": "^14.2.1", 13 | "eslint-plugin-import": "^2.22.1", 14 | "eslint-plugin-vue": "^7.7.0", 15 | "file-loader": "^6.0.0", 16 | "node-sass": "^5.0.0", 17 | "regenerator-runtime": "^0.13.7", 18 | "sass-loader": "^11.0.1", 19 | "vue": "^2.6.12", 20 | "vue-loader": "15.9.5", 21 | "vue-template-compiler": "^2.6.12", 22 | "webpack-notifier": "^1.13.0" 23 | }, 24 | "license": "UNLICENSED", 25 | "private": true, 26 | "scripts": { 27 | "dev-server": "encore dev-server", 28 | "dev": "encore dev", 29 | "watch": "encore dev --watch", 30 | "build": "encore production --progress", 31 | "lint": "node_modules/.bin/eslint assets/** --ignore-pattern *.scss --ignore-pattern *.gif --ignore-pattern *.png" 32 | }, 33 | "dependencies": { 34 | "pretty-checkbox": "^3.0.3", 35 | "pretty-checkbox-vue": "^1.1.9", 36 | "v-clipboard": "^2.2.3", 37 | "v-tooltip": "^2.0.3", 38 | "vue-router": "^3.4.9", 39 | "vuetify": "^2.3.21" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | bootEnv(dirname(__DIR__).'/.env'); 11 | 12 | if ($_SERVER['APP_DEBUG']) { 13 | umask(0000); 14 | 15 | Debug::enable(); 16 | } 17 | 18 | if ($trustedProxies = $_SERVER['TRUSTED_PROXIES'] ?? false) { 19 | Request::setTrustedProxies(explode(',', $trustedProxies), Request::HEADER_X_FORWARDED_ALL ^ Request::HEADER_X_FORWARDED_HOST); 20 | } 21 | 22 | if ($trustedHosts = $_SERVER['TRUSTED_HOSTS'] ?? false) { 23 | Request::setTrustedHosts([$trustedHosts]); 24 | } 25 | 26 | $kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']); 27 | $request = Request::createFromGlobals(); 28 | $response = $kernel->handle($request); 29 | $response->send(); 30 | $kernel->terminate($request, $response); 31 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/build/images/favicon/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/build/images/favicon/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /src/Controller/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bun4uk/chadmin/d6bbb9f9f7d3654102ad70f7368d046f3832b6ed/src/Controller/.gitignore -------------------------------------------------------------------------------- /src/Controller/QueriesController.php: -------------------------------------------------------------------------------- 1 | client->select('select * from system.clusters')->rows(); 28 | 29 | $processes = []; 30 | $processesQueries = []; 31 | 32 | for ($i = 0, $count = count($nodes); $i < $count; $i++) { 33 | $processesQueries[$i] = $client->client->selectAsync( 34 | <<client->getConnectUsername()}', '{$client->client->getConnectPassword()}') 40 | where elapsed >1 41 | SQL 42 | ); 43 | } 44 | $client->client->executeAsync(); 45 | for ($i = 0, $count = count($nodes); $i < $count; $i++) { 46 | try { 47 | $processes[$nodes[$i]['host_name']] = $processesQueries[$i]->rows(); 48 | } catch (QueryException $exception) { 49 | } 50 | } 51 | 52 | return $this->json(['cluster' => $cluster, 'processes' => $processes]); 53 | } 54 | 55 | /** 56 | * @param string $cluster 57 | * @param string $queryId 58 | * @param ClickHouseClient $client 59 | * @return Response 60 | * @Route("/api/kill-query/{queryId}", name="api_kill_query") 61 | */ 62 | public function apiKillQuery(string $cluster, string $queryId, ClickHouseClient $client): Response 63 | { 64 | try { 65 | $client->client->write("KILL QUERY ON CLUSTER {$cluster} WHERE query_id='{$queryId}'"); 66 | 67 | } catch (Exception $exception) { 68 | return $this->json(['status' => false, 'query_id' => $queryId]); 69 | } 70 | 71 | return $this->json(['status' => 'ok', 'query_id' => $queryId]); 72 | } 73 | 74 | /** 75 | * @param string $cluster 76 | * @return JsonResponse 77 | * @Route("/api/get-cluster-name", name="api_get_cluster_name") 78 | */ 79 | public function getCluster(string $cluster): JsonResponse 80 | { 81 | return $this->json(['cluster' => $cluster]); 82 | } 83 | 84 | /** 85 | * @param ClickHouseClient $client 86 | * @return JsonResponse 87 | * @Route("/api/get-cluster-list", name="api_get_cluster_list") 88 | */ 89 | public function getClusterList(ClickHouseClient $client): JsonResponse 90 | { 91 | $nodes = $client->client->select('SELECT DISTINCT cluster FROM system.clusters WHERE is_local = 1'); 92 | $clusterList = []; 93 | while (true) { 94 | $res = $nodes->fetchRow('cluster'); 95 | if ($res === null) { 96 | break; 97 | } 98 | $clusterList[] = $res; 99 | } 100 | 101 | return $this->json($clusterList); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/Controller/SPAQueriesController.php: -------------------------------------------------------------------------------- 1 | render('rq.html.twig', [ 26 | 'controller_name' => 'SPAQueriesController', 27 | ]); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Controller/UserController.php: -------------------------------------------------------------------------------- 1 | client->select('SELECT 22 | u.name, 23 | u.id, 24 | u.storage, 25 | groupArray(g.granted_role_name) AS roles 26 | FROM system.users AS u 27 | LEFT JOIN system.role_grants AS g ON g.user_name = u.name 28 | GROUP BY 29 | u.name, 30 | u.id, 31 | u.storage 32 | ORDER BY roles,name desc')->rows(); 33 | // dd($users); 34 | 35 | return $this->render('user_list.html.twig', ['users' => $users, 'user' => null]); 36 | } 37 | 38 | /** 39 | * @param string $userName 40 | * @param ClickHouseClient $client 41 | * @return Response 42 | * @Route("/user/{userName}", name="user") 43 | */ 44 | public function user(string $userName, ClickHouseClient $client): Response 45 | { 46 | 47 | $users = $client->client->selectAsync('SELECT 48 | u.name, 49 | u.id, 50 | u.storage, 51 | groupArray(g.granted_role_name) AS roles 52 | FROM system.users AS u 53 | LEFT JOIN system.role_grants AS g ON g.user_name = u.name 54 | GROUP BY 55 | u.name, 56 | u.id, 57 | u.storage 58 | ORDER BY roles desc'); 59 | 60 | $user = $client->client->selectAsync('SELECT 61 | u.name, 62 | u.id, 63 | u.storage, 64 | groupArray(g.granted_role_name) AS roles 65 | FROM system.users AS u 66 | LEFT JOIN system.role_grants AS g ON g.user_name = u.name 67 | WHERE u.name=:name 68 | GROUP BY 69 | u.name, 70 | u.id, 71 | u.storage 72 | ORDER BY u.name', ['name' => $userName]); 73 | 74 | $userGrants = $client->client->selectAsync('show grants for {user}', ['user' => $userName]); 75 | $roleGrants = []; 76 | $client->client->executeAsync(); 77 | $users = $users->rows(); 78 | $user = $user->fetchRow(); 79 | $user['grants'] = $userGrants->rows(); 80 | foreach ($user['roles'] as $role) { 81 | $roleGrants[$role] = $client->client->selectAsync('show grants for {role}', ['role' => $role]); 82 | } 83 | $client->client->executeAsync(); 84 | 85 | foreach ($roleGrants as $role => $roleGrant) { 86 | $roleGrant = $roleGrant->rows(); 87 | foreach ($roleGrant as $grants) { 88 | $user['grants'][] = $grants; 89 | } 90 | } 91 | 92 | return $this->render('user_list.html.twig', ['users' => $users, 'user' => $user]); 93 | } 94 | 95 | /** 96 | * @param ClickHouseClient $client 97 | * @return Response 98 | * @Route("/user/roles", name="roles") 99 | */ 100 | public function roles(ClickHouseClient $client): Response 101 | { 102 | 103 | $users = $client->client->select('select * from system.roles')->rows(); 104 | dd($users); 105 | $processes = []; 106 | $processesQueries = []; 107 | 108 | return $this->render('running_queries.html.twig', ['processes' => $processes]); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Kernel.php: -------------------------------------------------------------------------------- 1 | import('../config/{packages}/*.yaml'); 17 | $container->import('../config/{packages}/'.$this->environment.'/*.yaml'); 18 | 19 | if (is_file(\dirname(__DIR__).'/config/services.yaml')) { 20 | $container->import('../config/{services}.yaml'); 21 | $container->import('../config/{services}_'.$this->environment.'.yaml'); 22 | } elseif (is_file($path = \dirname(__DIR__).'/config/services.php')) { 23 | (require $path)($container->withPath($path), $this); 24 | } 25 | } 26 | 27 | protected function configureRoutes(RoutingConfigurator $routes): void 28 | { 29 | $routes->import('../config/{routes}/'.$this->environment.'/*.yaml'); 30 | $routes->import('../config/{routes}/*.yaml'); 31 | 32 | if (is_file(\dirname(__DIR__).'/config/routes.yaml')) { 33 | $routes->import('../config/{routes}.yaml'); 34 | } elseif (is_file($path = \dirname(__DIR__).'/config/routes.php')) { 35 | (require $path)($routes->withPath($path), $this); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Resolver/ClusterNameResolver.php: -------------------------------------------------------------------------------- 1 | clickHouseClient = $clickHouseClient->client; 32 | } 33 | 34 | /** 35 | * Whether this resolver can resolve the value for the given ArgumentMetadata. 36 | * 37 | * @param Request $request 38 | * @param ArgumentMetadata $argument 39 | * 40 | * @return bool 41 | */ 42 | public function supports(Request $request, ArgumentMetadata $argument): bool 43 | { 44 | 45 | return $argument->getType() === 'string' && $argument->getName() === 'cluster'; 46 | } 47 | 48 | /** 49 | * @param Request $request 50 | * @param ArgumentMetadata $argument 51 | * 52 | * @return Generator 53 | */ 54 | public function resolve( 55 | Request $request, 56 | ArgumentMetadata $argument 57 | ): Generator 58 | { 59 | yield $this->clickHouseClient->select('select cluster from system.clusters limit 1')->fetchRow('cluster'); 60 | } 61 | } -------------------------------------------------------------------------------- /src/Service/ClickHouseClient.php: -------------------------------------------------------------------------------- 1 | client = new Client( 31 | [ 32 | 'host' => $host, 33 | 'port' => $port, 34 | 'username' => $username, 35 | 'password' => $password 36 | ] 37 | ); 38 | $this->client->database($dbName); 39 | } 40 | } -------------------------------------------------------------------------------- /symfony.lock: -------------------------------------------------------------------------------- 1 | { 2 | "doctrine/annotations": { 3 | "version": "1.0", 4 | "recipe": { 5 | "repo": "github.com/symfony/recipes", 6 | "branch": "master", 7 | "version": "1.0", 8 | "ref": "a2759dd6123694c8d901d0ec80006e044c2e6457" 9 | }, 10 | "files": [ 11 | "config/routes/annotations.yaml" 12 | ] 13 | }, 14 | "doctrine/inflector": { 15 | "version": "1.4.3" 16 | }, 17 | "doctrine/lexer": { 18 | "version": "1.2.1" 19 | }, 20 | "monolog/monolog": { 21 | "version": "2.1.1" 22 | }, 23 | "nikic/php-parser": { 24 | "version": "v4.9.1" 25 | }, 26 | "php": { 27 | "version": "7.4" 28 | }, 29 | "psr/cache": { 30 | "version": "1.0.1" 31 | }, 32 | "psr/container": { 33 | "version": "1.0.0" 34 | }, 35 | "psr/event-dispatcher": { 36 | "version": "1.0.0" 37 | }, 38 | "psr/log": { 39 | "version": "1.1.3" 40 | }, 41 | "roave/security-advisories": { 42 | "version": "dev-master" 43 | }, 44 | "smi2/phpclickhouse": { 45 | "version": "1.3.9" 46 | }, 47 | "symfony/asset": { 48 | "version": "v5.1.6" 49 | }, 50 | "symfony/cache": { 51 | "version": "v5.1.5" 52 | }, 53 | "symfony/cache-contracts": { 54 | "version": "v2.2.0" 55 | }, 56 | "symfony/config": { 57 | "version": "v5.1.5" 58 | }, 59 | "symfony/console": { 60 | "version": "5.1", 61 | "recipe": { 62 | "repo": "github.com/symfony/recipes", 63 | "branch": "master", 64 | "version": "5.1", 65 | "ref": "c6d02bdfba9da13c22157520e32a602dbee8a75c" 66 | }, 67 | "files": [ 68 | "bin/console" 69 | ] 70 | }, 71 | "symfony/debug-bundle": { 72 | "version": "4.1", 73 | "recipe": { 74 | "repo": "github.com/symfony/recipes", 75 | "branch": "master", 76 | "version": "4.1", 77 | "ref": "f8863cbad2f2e58c4b65fa1eac892ab189971bea" 78 | }, 79 | "files": [ 80 | "config/packages/dev/debug.yaml" 81 | ] 82 | }, 83 | "symfony/debug-pack": { 84 | "version": "v1.0.8" 85 | }, 86 | "symfony/dependency-injection": { 87 | "version": "v5.1.5" 88 | }, 89 | "symfony/deprecation-contracts": { 90 | "version": "v2.2.0" 91 | }, 92 | "symfony/dotenv": { 93 | "version": "v5.1.5" 94 | }, 95 | "symfony/error-handler": { 96 | "version": "v5.1.5" 97 | }, 98 | "symfony/event-dispatcher": { 99 | "version": "v5.1.5" 100 | }, 101 | "symfony/event-dispatcher-contracts": { 102 | "version": "v2.2.0" 103 | }, 104 | "symfony/filesystem": { 105 | "version": "v5.1.5" 106 | }, 107 | "symfony/finder": { 108 | "version": "v5.1.5" 109 | }, 110 | "symfony/flex": { 111 | "version": "1.0", 112 | "recipe": { 113 | "repo": "github.com/symfony/recipes", 114 | "branch": "master", 115 | "version": "1.0", 116 | "ref": "c0eeb50665f0f77226616b6038a9b06c03752d8e" 117 | }, 118 | "files": [ 119 | ".env" 120 | ] 121 | }, 122 | "symfony/framework-bundle": { 123 | "version": "5.1", 124 | "recipe": { 125 | "repo": "github.com/symfony/recipes", 126 | "branch": "master", 127 | "version": "5.1", 128 | "ref": "e1b2770f2404d8307450a49cabfc3b2ff3184792" 129 | }, 130 | "files": [ 131 | "config/packages/cache.yaml", 132 | "config/packages/framework.yaml", 133 | "config/packages/test/framework.yaml", 134 | "config/routes/dev/framework.yaml", 135 | "config/services.yaml", 136 | "public/index.php", 137 | "src/Controller/.gitignore", 138 | "src/Kernel.php" 139 | ] 140 | }, 141 | "symfony/http-client": { 142 | "version": "v5.1.5" 143 | }, 144 | "symfony/http-client-contracts": { 145 | "version": "v2.2.0" 146 | }, 147 | "symfony/http-foundation": { 148 | "version": "v5.1.5" 149 | }, 150 | "symfony/http-kernel": { 151 | "version": "v5.1.5" 152 | }, 153 | "symfony/maker-bundle": { 154 | "version": "1.0", 155 | "recipe": { 156 | "repo": "github.com/symfony/recipes", 157 | "branch": "master", 158 | "version": "1.0", 159 | "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f" 160 | } 161 | }, 162 | "symfony/monolog-bridge": { 163 | "version": "v5.1.5" 164 | }, 165 | "symfony/monolog-bundle": { 166 | "version": "3.3", 167 | "recipe": { 168 | "repo": "github.com/symfony/recipes", 169 | "branch": "master", 170 | "version": "3.3", 171 | "ref": "d7249f7d560f6736115eee1851d02a65826f0a56" 172 | }, 173 | "files": [ 174 | "config/packages/dev/monolog.yaml", 175 | "config/packages/prod/deprecations.yaml", 176 | "config/packages/prod/monolog.yaml", 177 | "config/packages/test/monolog.yaml" 178 | ] 179 | }, 180 | "symfony/polyfill-intl-grapheme": { 181 | "version": "v1.18.1" 182 | }, 183 | "symfony/polyfill-intl-normalizer": { 184 | "version": "v1.18.1" 185 | }, 186 | "symfony/polyfill-mbstring": { 187 | "version": "v1.18.1" 188 | }, 189 | "symfony/polyfill-php73": { 190 | "version": "v1.18.1" 191 | }, 192 | "symfony/polyfill-php80": { 193 | "version": "v1.18.1" 194 | }, 195 | "symfony/profiler-pack": { 196 | "version": "v1.0.5" 197 | }, 198 | "symfony/routing": { 199 | "version": "5.1", 200 | "recipe": { 201 | "repo": "github.com/symfony/recipes", 202 | "branch": "master", 203 | "version": "5.1", 204 | "ref": "b4f3e7c95e38b606eef467e8a42a8408fc460c43" 205 | }, 206 | "files": [ 207 | "config/packages/prod/routing.yaml", 208 | "config/packages/routing.yaml", 209 | "config/routes.yaml" 210 | ] 211 | }, 212 | "symfony/service-contracts": { 213 | "version": "v2.2.0" 214 | }, 215 | "symfony/stopwatch": { 216 | "version": "v5.1.5" 217 | }, 218 | "symfony/string": { 219 | "version": "v5.1.5" 220 | }, 221 | "symfony/translation-contracts": { 222 | "version": "v2.2.0" 223 | }, 224 | "symfony/twig-bridge": { 225 | "version": "v5.1.5" 226 | }, 227 | "symfony/twig-bundle": { 228 | "version": "5.0", 229 | "recipe": { 230 | "repo": "github.com/symfony/recipes", 231 | "branch": "master", 232 | "version": "5.0", 233 | "ref": "fab9149bbaa4d5eca054ed93f9e1b66cc500895d" 234 | }, 235 | "files": [ 236 | "config/packages/test/twig.yaml", 237 | "config/packages/twig.yaml", 238 | "templates/base.html.twig" 239 | ] 240 | }, 241 | "symfony/twig-pack": { 242 | "version": "v1.0.0" 243 | }, 244 | "symfony/var-dumper": { 245 | "version": "v5.1.5" 246 | }, 247 | "symfony/var-exporter": { 248 | "version": "v5.1.5" 249 | }, 250 | "symfony/web-profiler-bundle": { 251 | "version": "3.3", 252 | "recipe": { 253 | "repo": "github.com/symfony/recipes", 254 | "branch": "master", 255 | "version": "3.3", 256 | "ref": "6bdfa1a95f6b2e677ab985cd1af2eae35d62e0f6" 257 | }, 258 | "files": [ 259 | "config/packages/dev/web_profiler.yaml", 260 | "config/packages/test/web_profiler.yaml", 261 | "config/routes/dev/web_profiler.yaml" 262 | ] 263 | }, 264 | "symfony/webpack-encore-bundle": { 265 | "version": "1.6", 266 | "recipe": { 267 | "repo": "github.com/symfony/recipes", 268 | "branch": "master", 269 | "version": "1.6", 270 | "ref": "69e1d805ad95964088bd510c05995e87dc391564" 271 | }, 272 | "files": [ 273 | "assets/css/app.css", 274 | "assets/js/app.js", 275 | "config/packages/assets.yaml", 276 | "config/packages/prod/webpack_encore.yaml", 277 | "config/packages/test/webpack_encore.yaml", 278 | "config/packages/webpack_encore.yaml", 279 | "package.json", 280 | "webpack.config.js" 281 | ] 282 | }, 283 | "symfony/yaml": { 284 | "version": "v5.1.5" 285 | }, 286 | "twig/extra-bundle": { 287 | "version": "v3.0.5" 288 | }, 289 | "twig/twig": { 290 | "version": "v3.0.5" 291 | } 292 | } 293 | -------------------------------------------------------------------------------- /templates/base_spa.html.twig: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}chadmin{% endblock %} 6 | {% block stylesheets %} 7 | {{ encore_entry_link_tags('app') }} 8 | {% endblock %} 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 27 | 28 | Clickhouse Running Queries 29 | 30 | 31 | 32 | 33 | {% block body %}{% endblock %} 34 | 35 | 36 | {% block javascripts %} 37 | {{ encore_entry_script_tags('app') }} 38 | {% endblock %} 39 | 40 | 41 | -------------------------------------------------------------------------------- /templates/rq.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base_spa.html.twig' %} 2 | 3 | {% block body %} 4 | 5 | 6 | {% endblock %} 7 | 8 | {% block javascripts %} 9 | {{ parent() }} 10 | 11 | {{ encore_entry_script_tags('app') }} 12 | {% endblock %} 13 | 14 | {% block stylesheets %} 15 | {{ parent() }} 16 | {{ encore_entry_link_tags('app') }} 17 | {% endblock %} -------------------------------------------------------------------------------- /templates/user_list.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'base.html.twig' %} 2 | {% block title %} 3 | Users 4 | {% endblock %} 5 | {% block body %} 6 | User List 7 | {# {{ dump() }}#} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ID 17 | 18 | 19 | Name 20 | 21 | 22 | Role 23 | 24 | 25 | 26 | 27 | 28 | 29 | {% for key,user in users %} 30 | 31 | 32 | 33 | {{ key+1 }} 34 | 35 | 36 | {{ user.name }} 37 | 38 | 39 | {% for role in user.roles %} 40 | {{ role }} 41 | {% endfor %} 42 | 43 | 44 | {% endfor %} 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | {% if user is not null %} 53 | {{ user.name }}'s grants: 54 | 55 | {% for grant in user.grants %} 56 | {% for rule in grant %} 57 | {{ rule }} 58 | {% endfor %} 59 | {% endfor %} 60 | 61 | {% endif %} 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | {% endblock %} -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const Encore = require('@symfony/webpack-encore'); 2 | const path = require('path'); 3 | 4 | // Manually configure the runtime environment if not already configured yet by the "encore" command. 5 | // It's useful when you use tools that rely on webpack.config.js file. 6 | if (!Encore.isRuntimeEnvironmentConfigured()) { 7 | Encore.configureRuntimeEnvironment(process.env.NODE_ENV || 'dev'); 8 | } 9 | 10 | Encore 11 | // directory where compiled assets will be stored 12 | .setOutputPath('public/build/') 13 | // public path used by the web server to access the output path 14 | .setPublicPath('/build') 15 | // only needed for CDN's or sub-directory deploy 16 | //.setManifestKeyPrefix('build/') 17 | 18 | /* 19 | * ENTRY CONFIG 20 | * 21 | * Add 1 entry for each "page" of your app 22 | * (including one that's included on every page - e.g. "app") 23 | * 24 | * Each entry will result in one JavaScript file (e.g. app.js) 25 | * and one CSS file (e.g. app.css) if your JavaScript imports CSS. 26 | */ 27 | .addEntry('app', './assets/js/app.js') 28 | // .addEntry('vue-router', './assets/js/router') 29 | //.addEntry('page1', './assets/js/page1.js') 30 | //.addEntry('page2', './assets/js/page2.js') 31 | 32 | // When enabled, Webpack "splits" your files into smaller pieces for greater optimization. 33 | .splitEntryChunks() 34 | 35 | // will require an extra script tag for runtime.js 36 | // but, you probably want this, unless you're building a single-page app 37 | .enableSingleRuntimeChunk() 38 | 39 | .addAliases({ 40 | '@': path.resolve(__dirname, 'assets', 'js'), 41 | styles: path.resolve(__dirname, 'assets', 'scss'), 42 | }) 43 | 44 | /* 45 | * FEATURE CONFIG 46 | * 47 | * Enable & configure other features below. For a full 48 | * list of features, see: 49 | * https://symfony.com/doc/current/frontend.html#adding-more-features 50 | */ 51 | .cleanupOutputBeforeBuild() 52 | .enableBuildNotifications() 53 | .enableSourceMaps(!Encore.isProduction()) 54 | // enables hashed filenames (e.g. app.abc123.css) 55 | .enableVersioning(Encore.isProduction()) 56 | 57 | // enables @babel/preset-env polyfills 58 | .configureBabelPresetEnv((config) => { 59 | config.useBuiltIns = 'usage'; 60 | config.corejs = 3; 61 | }) 62 | 63 | .copyFiles({ 64 | from: './assets/images', 65 | to: Encore.isProduction() 66 | ? 'images/[path][name].[hash:8].[ext]' 67 | : 'images/[path][name].[ext]', 68 | }) 69 | 70 | // enables Sass/SCSS support 71 | .enableSassLoader() 72 | // Enable .vue file processing 73 | .enableVueLoader() 74 | 75 | .configureCssLoader((config) => { 76 | if (!Encore.isProduction() && config.modules) { 77 | config.modules.localIdentName = '[name]_[local]_[hash:base64:5]'; 78 | } 79 | }) 80 | 81 | // uncomment if you use TypeScript 82 | //.enableTypeScriptLoader() 83 | 84 | // uncomment to get integrity="..." attributes on your script & link tags 85 | // requires WebpackEncoreBundle 1.4 or higher 86 | //.enableIntegrityHashes(Encore.isProduction()) 87 | 88 | // uncomment if you're having problems with a jQuery plugin 89 | //.autoProvidejQuery() 90 | 91 | // uncomment if you use API Platform Admin (composer req api-admin) 92 | //.enableReactPreset() 93 | //.addEntry('admin', './assets/js/admin.js') 94 | ; 95 | 96 | if (!Encore.isProduction()) { 97 | Encore.disableCssExtraction(); 98 | } 99 | 100 | module.exports = Encore.getWebpackConfig(); 101 | --------------------------------------------------------------------------------
9 | its just another vue route - you can change the url above to any thing 10 |