├── assets ├── CNAME └── 404.html ├── docs ├── CNAME ├── fonts │ ├── ui-grid.eot │ ├── ui-grid.ttf │ ├── ui-grid.woff │ ├── materialdesignicons-webfont.eot │ ├── materialdesignicons-webfont.ttf │ ├── materialdesignicons-webfont.woff │ ├── materialdesignicons-webfont.woff2 │ └── ui-grid.svg ├── assets │ └── images │ │ ├── bg.jpg │ │ └── logo.png ├── 404.html ├── index.html └── styles │ ├── app-9c669350e7.css │ └── app-7900bfb0cd.css ├── .bowerrc ├── gulp ├── .eslintrc ├── scripts.js ├── favicon.js ├── watch.js ├── documentation.js ├── styles.js ├── inject.js ├── server.js ├── conf.js └── build.js ├── media ├── screen1.png ├── screen2.png ├── screen3.png ├── screen4.jpg └── screen5.jpg ├── src ├── assets │ └── images │ │ ├── bg.jpg │ │ └── logo.png ├── app │ ├── table │ │ ├── table.html │ │ ├── table.js │ │ ├── table-container.js │ │ └── table-container.html │ ├── base │ │ ├── 404.html │ │ ├── breadcrumbs.html │ │ ├── header.js │ │ ├── base.html │ │ ├── sidebar.html │ │ ├── header.html │ │ └── sidebar.js │ ├── index.app.js │ ├── index.run.js │ ├── filters │ │ └── filesize.js │ ├── services │ │ ├── http_interceptor.js │ │ ├── theme.js │ │ └── api.js │ ├── login │ │ ├── login.js │ │ └── login.html │ ├── index.route.js │ ├── index.config.js │ └── sql │ │ ├── ace-mode-clickhouse.js │ │ ├── sql.html │ │ └── sql.js ├── less │ ├── theme.less │ ├── login.less │ ├── index.less │ ├── sidebar.less │ ├── layout.less │ └── core.less └── index.html ├── .gitignore ├── README.md ├── .dockerignore ├── .editorconfig ├── Dockerfile ├── gulpfile.js ├── .eslintrc ├── LICENSE.txt ├── bower.json └── package.json /assets/CNAME: -------------------------------------------------------------------------------- 1 | guiclickhouse.smi2.ru -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | guiclickhouse.smi2.ru -------------------------------------------------------------------------------- /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components" 3 | } 4 | -------------------------------------------------------------------------------- /gulp/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /media/screen1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smi2/clickhouse-frontend/HEAD/media/screen1.png -------------------------------------------------------------------------------- /media/screen2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smi2/clickhouse-frontend/HEAD/media/screen2.png -------------------------------------------------------------------------------- /media/screen3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smi2/clickhouse-frontend/HEAD/media/screen3.png -------------------------------------------------------------------------------- /media/screen4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smi2/clickhouse-frontend/HEAD/media/screen4.jpg -------------------------------------------------------------------------------- /media/screen5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smi2/clickhouse-frontend/HEAD/media/screen5.jpg -------------------------------------------------------------------------------- /docs/fonts/ui-grid.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smi2/clickhouse-frontend/HEAD/docs/fonts/ui-grid.eot -------------------------------------------------------------------------------- /docs/fonts/ui-grid.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smi2/clickhouse-frontend/HEAD/docs/fonts/ui-grid.ttf -------------------------------------------------------------------------------- /docs/fonts/ui-grid.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smi2/clickhouse-frontend/HEAD/docs/fonts/ui-grid.woff -------------------------------------------------------------------------------- /src/assets/images/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smi2/clickhouse-frontend/HEAD/src/assets/images/bg.jpg -------------------------------------------------------------------------------- /docs/assets/images/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smi2/clickhouse-frontend/HEAD/docs/assets/images/bg.jpg -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smi2/clickhouse-frontend/HEAD/src/assets/images/logo.png -------------------------------------------------------------------------------- /docs/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smi2/clickhouse-frontend/HEAD/docs/assets/images/logo.png -------------------------------------------------------------------------------- /docs/fonts/materialdesignicons-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smi2/clickhouse-frontend/HEAD/docs/fonts/materialdesignicons-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/materialdesignicons-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smi2/clickhouse-frontend/HEAD/docs/fonts/materialdesignicons-webfont.ttf -------------------------------------------------------------------------------- /docs/fonts/materialdesignicons-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smi2/clickhouse-frontend/HEAD/docs/fonts/materialdesignicons-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/materialdesignicons-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smi2/clickhouse-frontend/HEAD/docs/fonts/materialdesignicons-webfont.woff2 -------------------------------------------------------------------------------- /src/app/table/table.html: -------------------------------------------------------------------------------- 1 |
2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bower_components 2 | node_modules 3 | .tmp 4 | coverage 5 | help 6 | 7 | # IntelliJ project files 8 | .idea 9 | *.iml 10 | out 11 | gen 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **Project rename TABIX, this repo depreciation** 2 | 3 | 4 | https://tabix.io 5 | 6 | https://tabix.io/doc/ 7 | 8 | 9 | https://github.com/smi2/tabix.ui 10 | 11 | ----- 12 | ----- 13 | ----- 14 | ----- 15 | ----- 16 | 17 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .git/ 3 | .tmp/ 4 | .dockerignore 5 | Dockerfile* 6 | bower_components/ 7 | docker-compose.* 8 | 9 | .gitignore 10 | .env 11 | .editorconfig 12 | .gitlab-ci.yml 13 | .idea/ 14 | .vagrant/ 15 | .vscode/ 16 | todo 17 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /assets/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | СМИ2 clickhouse 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /docs/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | СМИ2 clickhouse 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/app/base/404.html: -------------------------------------------------------------------------------- 1 |
2 |

404 {{'не найдено' | translate}}

3 | 4 | 5 | {{ 'Назад' | translate }} 6 | 7 |
8 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:6.7.0 2 | 3 | RUN npm install -g bower && npm update -g bower && \ 4 | npm install -g gulp && npm update -g gulp 5 | 6 | COPY . /app 7 | 8 | WORKDIR /app 9 | 10 | RUN npm install && \ 11 | bower install --allow-root && \ 12 | gulp build 13 | 14 | EXPOSE 3000 15 | 16 | CMD ["gulp", "serve:dist"] 17 | -------------------------------------------------------------------------------- /src/less/theme.less: -------------------------------------------------------------------------------- 1 | @border: #4d4d4d; 2 | //@light: #dddddd; 3 | 4 | md-input-container { 5 | margin: 4px 0px; 6 | } 7 | 8 | a { 9 | color: inherit; 10 | text-decoration: none; 11 | } 12 | 13 | md-toast.md-dark-theme { 14 | .md-toast-content { 15 | background-color: #666; 16 | } 17 | } 18 | 19 | md-card { 20 | border-radius: 0; 21 | } 22 | -------------------------------------------------------------------------------- /src/app/base/breadcrumbs.html: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /src/app/table/table.js: -------------------------------------------------------------------------------- 1 | (( angular, smi2 ) => { 2 | 'use strict'; 3 | 4 | angular.module( smi2.app.name ).controller( 'TableController', TableController ); 5 | TableController.$inject = [ '$scope', '$stateParams' ]; 6 | 7 | /** 8 | * @ngdoc controller 9 | * @name smi2.controller:TableController 10 | * @description Контроллер страницы 1 таблицы БД 11 | */ 12 | function TableController( $scope, $stateParams ) { 13 | let { dbName, tableName } = $stateParams; 14 | $scope.vars = { 15 | dbName, 16 | tableName 17 | }; 18 | 19 | } 20 | })( angular, smi2 ); 21 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Welcome to your gulpfile! 3 | * The gulp tasks are splitted in several files in the gulp directory 4 | * because putting all here was really too long 5 | */ 6 | 7 | 'use strict'; 8 | 9 | var gulp = require('gulp'); 10 | var glob = require('glob'); 11 | 12 | /** 13 | * This will load all js or coffee files in the gulp directory 14 | * in order to load all gulp tasks 15 | */ 16 | glob.sync('./gulp/**/*.js').forEach(function (file) { 17 | require(file); 18 | }); 19 | 20 | /** 21 | * Default task clean temporaries directories and launch the 22 | * main optimization build task 23 | */ 24 | gulp.task('default', ['clean'], function () { 25 | gulp.start('build'); 26 | }); 27 | -------------------------------------------------------------------------------- /src/app/index.app.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | 'use strict'; 3 | 4 | var smi2 = window.smi2 = window.smi2 || {}; 5 | smi2.app = { 6 | name: 'SMI2', 7 | version: window.clickhouseGuiVersion || "" 8 | }; 9 | 10 | // External libs connection 11 | angular.module(smi2.app.name, [ 12 | 'ngAnimate', 13 | 'ui.router', 14 | 'LocalStorageModule', 15 | 'angularScreenfull', 16 | 'ui.ace', 17 | 'ui.grid', 18 | 'ui.grid.autoResize', 19 | 'angularResizable', 20 | 'ngSanitize', 21 | 'ngMaterial', 22 | 'funMetisMenu', 23 | 'ngScrollbars', 24 | 'ngCsv', 25 | 'pascalprecht.translate' 26 | ]); 27 | })(); 28 | -------------------------------------------------------------------------------- /gulp/scripts.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var gulp = require('gulp'); 5 | var conf = require('./conf'); 6 | 7 | var browserSync = require('browser-sync'); 8 | 9 | var $ = require('gulp-load-plugins')(); 10 | 11 | 12 | gulp.task('scripts-reload', function() { 13 | return buildScripts() 14 | .pipe(browserSync.stream()); 15 | }); 16 | 17 | gulp.task('scripts', function() { 18 | return buildScripts(); 19 | }); 20 | 21 | function buildScripts() { 22 | return gulp.src(path.join(conf.paths.src, '/app/**/*.js')) 23 | .pipe($.eslint()) 24 | .pipe($.eslint.format()) 25 | .pipe($.babel({ 26 | presets: ['es2015', 'stage-0'] 27 | })) 28 | .on('error', conf.errorHandler('babel')) 29 | .pipe($.size()) 30 | }; 31 | -------------------------------------------------------------------------------- /src/app/index.run.js: -------------------------------------------------------------------------------- 1 | (() => { 2 | 'use strict'; 3 | 4 | /** 5 | * @ngdoc controller 6 | * @name smi2.controller:Run 7 | * @description Main APP controller 8 | */ 9 | angular 10 | .module(smi2.app.name) 11 | .run([ 12 | '$rootScope', 13 | '$state', 14 | ($rootScope, $state) => { 15 | 16 | $rootScope.breadcrumbs = []; 17 | $rootScope.currentDatabase = null; 18 | 19 | // Провеярю в чем ошибка перехода на state 20 | var stateChangeErrorUnbind = $rootScope.$on('$stateChangeError', (toState, toParams, fromState, fromParams, error, reason) => { 21 | if (reason == 'notAuthorized') { 22 | $state.go('login'); 23 | } 24 | }); 25 | 26 | // Требование JSlinter'a ((( 27 | $rootScope.$on('$destroy', () => stateChangeErrorUnbind); 28 | } 29 | ]); 30 | })(); 31 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | СМИ2 clickhouse
-------------------------------------------------------------------------------- /src/app/base/header.js: -------------------------------------------------------------------------------- 1 | ((angular, smi2) => { 2 | 'use strict'; 3 | 4 | angular.module(smi2.app.name).controller('HeaderController', HeaderController); 5 | HeaderController.$inject = ['$scope', '$state', 'API', 'ThemeService']; 6 | 7 | /** 8 | * @ngdoc controller 9 | * @name smi2.controller:HeaderController 10 | * @description Controller for layout header 11 | */ 12 | function HeaderController($scope, $state, API, ThemeService) { 13 | $scope.user = API.getConnectionInfo().name; 14 | $scope.themes = ThemeService.list; 15 | 16 | /** 17 | * Logout ) 18 | */ 19 | $scope.logout = () => { 20 | API.clear(); 21 | $state.go('login'); 22 | }; 23 | 24 | /** 25 | * Change UI theme 26 | * @param theme 27 | */ 28 | $scope.setUiTheme = (theme) => ThemeService.set(theme.name); 29 | } 30 | })(angular, smi2); 31 | -------------------------------------------------------------------------------- /src/app/filters/filesize.js: -------------------------------------------------------------------------------- 1 | ((angular, smi2) => { 2 | 'use strict'; 3 | 4 | angular 5 | .module(smi2.app.name) 6 | .filter('filesize', () => { 7 | var units = [ 8 | 'bytes', 9 | 'KB', 10 | 'MB', 11 | 'GB', 12 | 'TB', 13 | 'PB' 14 | ]; 15 | 16 | return (bytes, precision) => { 17 | if (isNaN(parseFloat(bytes)) || !isFinite(bytes)) { 18 | return '?'; 19 | } 20 | 21 | var unit = 0; 22 | 23 | while (bytes >= 1024) { 24 | bytes /= 1024; 25 | unit++; 26 | } 27 | 28 | return bytes.toFixed(+precision) + ' ' + units[unit]; 29 | }; 30 | }); 31 | })(angular, smi2); 32 | -------------------------------------------------------------------------------- /src/app/services/http_interceptor.js: -------------------------------------------------------------------------------- 1 | ((angular, smi2) => { 2 | 'use strict'; 3 | 4 | angular.module(smi2.app.name).service('HttpInterceptor', HttpInterceptor); 5 | HttpInterceptor.$inject = ['$q', '$injector']; 6 | 7 | /** 8 | * @ngdoc service 9 | * @name smi2.service:HttpInterceptor 10 | * @description 11 | * Service from handle HTTP requests 12 | */ 13 | function HttpInterceptor($q, $injector) { 14 | return { 15 | /** 16 | * @ngdoc 17 | * @name responseError 18 | * @description Handle HTTP errors 19 | * @param {mixed} rejection 20 | * @methodOf smi2.service:HttpInterceptor 21 | * @return {promise} $q promise 22 | */ 23 | responseError: (rejection) => { 24 | 25 | // If not authorized -> go to login page 26 | if (rejection.status == 401) { 27 | $injector.get('$state').go('login'); 28 | } 29 | return $q.reject(rejection); 30 | } 31 | }; 32 | } 33 | })(angular, smi2); 34 | -------------------------------------------------------------------------------- /gulp/favicon.js: -------------------------------------------------------------------------------- 1 | var favicons = require('gulp-favicons'), 2 | gutil = require("gulp-util"); 3 | var path = require('path'); 4 | var gulp = require('gulp'); 5 | var conf = require('./conf'); 6 | 7 | gulp.task("favicons", function() { 8 | return gulp.src(path.join(conf.paths.src, '/assets/images/test-favicon.png')).pipe(favicons({ 9 | path: "favicons/", 10 | display: "standalone", 11 | orientation: "portrait", 12 | online: false, 13 | html: false, 14 | replace: true, 15 | icons: { 16 | android: false, 17 | appleIcon: false, 18 | appleStartup: false, 19 | coast: false, 20 | favicons: true, 21 | firefox: false, 22 | opengraph: false, 23 | twitter: false, 24 | windows: false, 25 | yandex: false 26 | } 27 | })) 28 | .on("error", gutil.log) 29 | .pipe(gulp.dest(path.join(conf.paths.tmp, '/favicons/localhost/'))); 30 | }); 31 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "plugins": ["angular", "es6-recommended"], 4 | "env": { 5 | "browser": true, 6 | "jasmine": true, 7 | "es6": true 8 | }, 9 | "globals": { 10 | "angular": true, 11 | "smi2": true, 12 | "module": true, 13 | "inject": true, 14 | "moment": true 15 | }, 16 | "rules": { 17 | "semi": 2, 18 | "angular/window-service":0, 19 | "angular/on-watch":0, 20 | 21 | // Нет объективных причин почему вместо $scope используется this. 22 | // Никаких изменений в производительности, памяти. 23 | // Хотя JohnPapa настаивает на this 24 | // https://github.com/johnpapa/angular-styleguide/blob/master/a1/README.md#style-y031 25 | "angular/controller-as": 0, 26 | "angular/controller-as-route":0, 27 | 28 | "angular/module-getter":0, 29 | "angular/di":0, 30 | "angular/log":0, 31 | "no-console":0 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2011-2016 Smi2, Inc. 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. -------------------------------------------------------------------------------- /src/less/login.less: -------------------------------------------------------------------------------- 1 | .login-view { 2 | position: relative; 3 | } 4 | .login-view .login-checking { 5 | display: -webkit-box; 6 | display: -ms-flexbox; 7 | display: flex; 8 | -webkit-box-orient: vertical; 9 | -webkit-box-direction: normal; 10 | -ms-flex-direction: column; 11 | flex-direction: column; 12 | -webkit-box-pack: center; 13 | -ms-flex-pack: center; 14 | justify-content: center; 15 | -webkit-box-align: center; 16 | -ms-flex-align: center; 17 | align-items: center; 18 | z-index: 10; 19 | background: rgba(0,0,0,0.6); 20 | text-align: center; 21 | position: absolute; 22 | width: 100%; 23 | height: 100%; 24 | } 25 | .login-view .login-checking .loading-text { 26 | -webkit-animation: pulse 0.8s linear infinite; 27 | animation: pulse 0.8s linear infinite; 28 | } 29 | .login-view .card__header { 30 | border-bottom: 1px solid rgba(0,0,0,0.2); 31 | } 32 | .login-view .login-message { 33 | padding: 8px; 34 | } 35 | .login-view .login-message.error { 36 | background-color: #f44336; 37 | } 38 | .login-view .login-message.success { 39 | background-color: #4caf50; 40 | } 41 | -------------------------------------------------------------------------------- /src/app/base/base.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 5 |
6 |
7 | 8 | 9 | 10 | 11 | 12 | {{'Открыть таблицу' | translate}} 13 | 14 | 15 |
16 |
17 |
18 |
19 |
20 |
21 | -------------------------------------------------------------------------------- /src/less/index.less: -------------------------------------------------------------------------------- 1 | @import 'core.less'; 2 | @import 'layout.less'; 3 | @import 'login.less'; 4 | @import 'theme.less'; 5 | @import 'sidebar.less'; 6 | 7 | .logo { 8 | width: 24px; 9 | height: 24px; 10 | margin-left: 10px; 11 | float: left; 12 | padding-top: 7px; 13 | } 14 | 15 | a { 16 | text-decoration: none; 17 | } 18 | 19 | .sql-table { 20 | th { 21 | background-color: rgba(200,200,200,0.2); 22 | } 23 | td, th { 24 | white-space: nowrap; 25 | text-align: left; 26 | padding: 5px; 27 | } 28 | .sql-table__subheader { 29 | font-size: 80%; 30 | opacity: 0.7; 31 | } 32 | tr:nth-child(even) { 33 | ///background: #f2f2f2; 34 | } 35 | } 36 | 37 | pre { 38 | tab-size: 2; 39 | } 40 | 41 | .fullscreen { 42 | width: 100%; 43 | height: 100%; 44 | &.default { 45 | background-color: rgb(250,250,250); 46 | } 47 | &.dark { 48 | background-color: rgb(48,48,48); 49 | } 50 | } 51 | 52 | *:-webkit-full-screen { 53 | background-color: inherit; 54 | } 55 | 56 | .sql-tabs { 57 | md-tabs-wrapper { 58 | padding-right: 48px; 59 | } 60 | md-next-button { 61 | right: 48px; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /gulp/watch.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var gulp = require('gulp'); 5 | var conf = require('./conf'); 6 | 7 | var browserSync = require('browser-sync'); 8 | 9 | function isOnlyChange(event) { 10 | return event.type === 'changed'; 11 | } 12 | 13 | gulp.task('watch', ['favicons','inject'], function () { 14 | 15 | gulp.watch([path.join(conf.paths.src, '/*.html'), 'bower.json'], ['inject-reload']); 16 | 17 | gulp.watch([ 18 | path.join(conf.paths.src, '/less/**/*.css'), 19 | path.join(conf.paths.src, '/less/**/*.less') 20 | ], function(event) { 21 | if(isOnlyChange(event)) { 22 | gulp.start('styles-reload'); 23 | } else { 24 | gulp.start('inject-reload'); 25 | } 26 | }); 27 | 28 | gulp.watch(path.join(conf.paths.src, '/app/**/*.js'), function(event) { 29 | if(isOnlyChange(event)) { 30 | gulp.start('scripts-reload'); 31 | } else { 32 | gulp.start('inject-reload'); 33 | } 34 | }); 35 | 36 | gulp.watch(path.join(conf.paths.src, '/app/assets/images/favicons/**/*'), function(event) { 37 | gulp.start('favicons'); 38 | }); 39 | 40 | 41 | gulp.watch(path.join(conf.paths.src, '/app/**/*.{json,html}'), function(event) { 42 | browserSync.reload(event.path); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/app/base/sidebar.html: -------------------------------------------------------------------------------- 1 | 25 | -------------------------------------------------------------------------------- /src/less/sidebar.less: -------------------------------------------------------------------------------- 1 | .sidebar-view { 2 | &.login{ 3 | z-index:3 4 | } 5 | margin-right: 14px; 6 | margin-top: 40px; 7 | 8 | .sidebar { 9 | overflow: hidden; 10 | height: 100%; 11 | padding-left: 5px; 12 | 13 | .mCSB_container_wrapper { 14 | margin-right: 5px !important; 15 | 16 | .mCSB_container { 17 | padding-right: 0 !important; 18 | } 19 | } 20 | 21 | .metismenu { 22 | -webkit-margin-before: 0; 23 | -webkit-padding-start: 0; 24 | 25 | ul { 26 | list-style: none; 27 | 28 | li { 29 | position: relative; 30 | padding-right: 5px; 31 | 32 | ul.sub-menu { 33 | -webkit-padding-start: 0; 34 | margin-left: 20px; 35 | } 36 | 37 | button { 38 | width: 100%; 39 | text-overflow: ellipsis; 40 | margin: 0; 41 | 42 | i { 43 | margin-right: 5px; 44 | } 45 | } 46 | } 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/app/services/theme.js: -------------------------------------------------------------------------------- 1 | ((angular, smi2) => { 2 | 'use strict'; 3 | 4 | angular.module(smi2.app.name).service('ThemeService', ThemeService); 5 | ThemeService.$inject = ['localStorageService', '$filter']; 6 | 7 | /** 8 | * @ngdoc service 9 | * @name smi2.service:ThemeService 10 | * @description Theme service 11 | */ 12 | function ThemeService(localStorageService, $filter) { 13 | 14 | const themeName = localStorageService.get('themeName') || 'dark'; 15 | const list = [{ 16 | isDark: false, 17 | name: 'default', 18 | title: $filter('translate')('Светлая тема') 19 | }, { 20 | isDark: true, 21 | name: 'dark', 22 | title: $filter('translate')('Темная тема') 23 | }]; 24 | let theme = list.find((item) => (item.name == themeName)) || list[0]; 25 | 26 | return { 27 | isDark: () => theme.isDark, 28 | theme: theme.name, 29 | themeObject: theme, 30 | set: (name) => { 31 | if (name != themeName) { 32 | localStorageService.set('themeName', name); 33 | location.href = location.href; 34 | } 35 | }, 36 | list 37 | }; 38 | } 39 | })(angular, smi2); 40 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SMI2-clickhouse", 3 | "version": "1.0.0", 4 | "dependencies": { 5 | "angular-animate": "~1.5.0", 6 | "jquery": "~2.1.4", 7 | "angular-ui-router": "~0.2.15", 8 | "angular": "~1.4.12", 9 | "angular-ui-ace": "~0.2.3", 10 | "angular-local-storage": "~0.2.7", 11 | "angular-screenfull": "^0.1.1", 12 | "angular-ui-grid": "^3.2.9", 13 | "screenfull": "~3.0.2", 14 | "angular-sanitize": "^1.5.8", 15 | "angular-material": "^1.1.1", 16 | "mdi": "^1.7.22", 17 | "ng-scrollbars": "^0.0.11", 18 | "fun-metis-menu": "*", 19 | "angular-resizable": "^1.2.0", 20 | "ng-csv": "^0.3.6", 21 | "angular-translate": "^2.12.1", 22 | "angular-translate-handler-log": "^2.13.1" 23 | }, 24 | "devDependencies": { 25 | "angular-mocks": "~1.5.0" 26 | }, 27 | "overrides": { 28 | "screenfull": { 29 | "main": [ 30 | "dist/screenfull.min.js" 31 | ] 32 | }, 33 | "mdi": { 34 | "main": [ 35 | "css/materialdesignicons.css", 36 | "fonts/*" 37 | ] 38 | }, 39 | "angular-resizable": { 40 | "main": [ 41 | "src/angular-resizable.js", 42 | "src/angular-resizable.css" 43 | ] 44 | } 45 | }, 46 | "resolutions": { 47 | "jquery": "~2.1.4", 48 | "angular": "~1.4.12" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /gulp/documentation.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var gulp = require('gulp'); 4 | var ngdocs = require('gulp-ngdocs'); 5 | var conf = require('./conf'); 6 | var path = require('path'); 7 | var browserSync = require('browser-sync'); 8 | var browserSyncSpa = require('browser-sync-spa'); 9 | 10 | /** 11 | * Генерация основы ngDoc приложения 12 | */ 13 | gulp.task('docs:generate', [], function() { 14 | return gulp.src('./src/**/*.js') 15 | .pipe(ngdocs.process({ 16 | scripts: [ 17 | 'bower_components/angular/angular.js', 18 | 'bower_components/angular-animate/angular-animate.min.js' 19 | ], 20 | loadDefaults: {}, 21 | html5Mode: false, 22 | startPage: '/api', 23 | title: "Документация Clickhouse Frontend", 24 | image: "https://smi2.net/wp-content/uploads/2016/02/icon-smi2.png", 25 | imageLink: "http://smi2.net", 26 | titleLink: "/api" 27 | })) 28 | .pipe(gulp.dest('./' + conf.paths.docs)); 29 | }); 30 | 31 | /** 32 | * Запуск сервера, по-другому приложение 33 | * запускаться отказалось 34 | */ 35 | gulp.task('docs', ['docs:generate'], function() { 36 | browserSync.use(browserSyncSpa({ 37 | selector: '[ng-app]' // Only needed for angular apps 38 | })); 39 | 40 | browserSync.instance = browserSync.init({ 41 | startPath: '/', 42 | server: { 43 | baseDir: './' + conf.paths.docs 44 | }, 45 | port: 3010, 46 | browser: 'default' 47 | }); 48 | gulp.watch(path.join(conf.paths.src, '/**/*.js'), function(event) { 49 | gulp.start('docs:generate'); 50 | browserSync.reload(event.path); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /gulp/styles.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var gulp = require('gulp'); 5 | var conf = require('./conf'); 6 | 7 | var browserSync = require('browser-sync'); 8 | 9 | var $ = require('gulp-load-plugins')(); 10 | 11 | var wiredep = require('wiredep').stream; 12 | var _ = require('lodash'); 13 | 14 | gulp.task('styles-reload', ['styles'], function() { 15 | return buildStyles() 16 | .pipe(browserSync.stream()); 17 | }); 18 | 19 | gulp.task('styles', function() { 20 | return buildStyles(); 21 | }); 22 | 23 | var buildStyles = function() { 24 | var lessOptions = { 25 | options: [ 26 | 'bower_components', 27 | path.join(conf.paths.src, '/') 28 | ] 29 | }; 30 | 31 | var injectFiles = gulp.src([ 32 | path.join(conf.paths.src, '/less/*.less'), 33 | path.join('!' + conf.paths.src, '/less/index.less') 34 | ], { read: false }); 35 | 36 | var injectOptions = { 37 | transform: function(filePath) { 38 | filePath = filePath.replace(conf.paths.src + '/less/', ''); 39 | return '@import "' + filePath + '";'; 40 | }, 41 | starttag: '// injector', 42 | endtag: '// endinjector', 43 | addRootSlash: false 44 | }; 45 | 46 | 47 | return gulp.src([ 48 | path.join(conf.paths.src, '/less/index.less') 49 | ]) 50 | .pipe($.inject(injectFiles, injectOptions)) 51 | .pipe(wiredep(_.extend({}, conf.wiredep))) 52 | .pipe($.sourcemaps.init()) 53 | .pipe($.less(lessOptions)).on('error', conf.errorHandler('Less')) 54 | .pipe($.autoprefixer()).on('error', conf.errorHandler('Autoprefixer')) 55 | .pipe($.sourcemaps.write()) 56 | .pipe(gulp.dest(path.join(conf.paths.tmp, '/serve/app/'))); 57 | }; 58 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | СМИ2 clickhouse 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 30 | 31 | 32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /gulp/inject.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var gulp = require('gulp'); 5 | var conf = require('./conf'); 6 | var packageJson = require('../package.json'); 7 | 8 | var $ = require('gulp-load-plugins')(); 9 | 10 | var wiredep = require('wiredep').stream; 11 | var _ = require('lodash'); 12 | 13 | var browserSync = require('browser-sync'); 14 | 15 | gulp.task('inject-reload', ['inject'], function() { 16 | browserSync.reload(); 17 | }); 18 | 19 | gulp.task('inject', ['scripts', 'styles'], function() { 20 | var injectStyles = gulp.src([ 21 | path.join(conf.paths.tmp, '/serve/app/**/*.css'), 22 | path.join('!' + conf.paths.tmp, '/serve/app/vendor.css') 23 | ], { 24 | read: false 25 | }); 26 | 27 | var injectScripts = gulp.src([ 28 | path.join(conf.paths.src, '/app/**/*.app.js'), 29 | path.join(conf.paths.src, '/app/**/*.js'), 30 | path.join('!' + conf.paths.src, '/app/**/*.test.js'), 31 | path.join('!' + conf.paths.src, '/app/**/*.mock.js'), 32 | ]) 33 | .pipe($.babel({ 34 | presets: ['es2015', 'stage-0'] 35 | })) 36 | .on('error', conf.errorHandler('babel')) 37 | .pipe($.angularFilesort()) 38 | .on('error', conf.errorHandler('AngularFilesort')); 39 | 40 | var injectOptions = { 41 | ignorePath: [conf.paths.src, path.join(conf.paths.tmp, '/serve')], 42 | addRootSlash: false 43 | }; 44 | 45 | return gulp.src(path.join(conf.paths.src, '/*.html')) 46 | .pipe($.inject(injectStyles, injectOptions)) 47 | .pipe($.inject(injectScripts, injectOptions)) 48 | .pipe($.replace('', '')) 49 | .pipe(wiredep(_.extend({}, conf.wiredep))) 50 | .pipe(gulp.dest(path.join(conf.paths.tmp, '/serve'))); 51 | }); 52 | -------------------------------------------------------------------------------- /src/app/base/header.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 8 | 9 |
10 | 11 | 12 | 13 | {{'Тема' | translate}} 14 | 15 | 16 | 17 | {{theme.title}} 18 | 19 | 20 | 21 | 22 | 23 | SQL 24 | 25 | 26 | 27 | {{'Выход' | translate}} 28 | 29 | {{user}} 30 |
31 |
32 |
33 |
34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SMI2-clickhouse", 3 | "version": "1.4.8", 4 | "license": "MIT", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/smi2/clickhouse-frontend.git" 8 | }, 9 | "dependencies": { 10 | "glob": "^7.0.3", 11 | "gulp-ngdocs": "^0.2.13", 12 | "marked": "^0.3.5" 13 | }, 14 | "scripts": { 15 | "test": "gulp test" 16 | }, 17 | "devDependencies": { 18 | "babel-preset-es2015": "^6.18.0", 19 | "babel-preset-stage-0": "^6.16.0", 20 | "browser-sync": "~2.9.11", 21 | "browser-sync-spa": "~1.0.3", 22 | "chalk": "~1.1.1", 23 | "del": "~2.0.2", 24 | "eslint-plugin-angular": "~0.12.0", 25 | "eslint-plugin-es6-recommended": "^0.1.2", 26 | "estraverse": "~4.1.0", 27 | "gulp": "~3.9.0", 28 | "gulp-angular-filesort": "~1.1.1", 29 | "gulp-angular-templatecache": "~1.8.0", 30 | "gulp-autoprefixer": "~3.0.2", 31 | "gulp-babel": "^6.1.2", 32 | "gulp-eslint": "~1.0.0", 33 | "gulp-favicons": "^2.2.6", 34 | "gulp-filter": "~3.0.1", 35 | "gulp-flatten": "~0.2.0", 36 | "gulp-inject": "~3.0.0", 37 | "gulp-less": "~3.0.3", 38 | "gulp-load-plugins": "~0.10.0", 39 | "gulp-minify-css": "~1.2.1", 40 | "gulp-minify-html": "~1.0.4", 41 | "gulp-ng-annotate": "~1.1.0", 42 | "gulp-rename": "~1.2.2", 43 | "gulp-replace": "~0.5.4", 44 | "gulp-rev": "~6.0.1", 45 | "gulp-rev-replace": "~0.4.2", 46 | "gulp-size": "~2.0.0", 47 | "gulp-sourcemaps": "~1.6.0", 48 | "gulp-uglify": "~1.4.1", 49 | "gulp-useref": "~1.3.0", 50 | "gulp-util": "~3.0.6", 51 | "lodash": "~3.10.1", 52 | "main-bower-files": "~2.9.0", 53 | "uglify-save-license": "~0.4.1", 54 | "wiredep": "~2.2.2" 55 | }, 56 | "engines": { 57 | "node": ">=0.10.0" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /gulp/server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var gulp = require('gulp'); 5 | var conf = require('./conf'); 6 | var browserSync = require('browser-sync'); 7 | var browserSyncSpa = require('browser-sync-spa'); 8 | 9 | var util = require('util'); 10 | 11 | function browserSyncInit(baseDir, browser) { 12 | browser = browser === undefined ? 'default' : browser; 13 | 14 | var routes = null; 15 | if(baseDir === conf.paths.src || (util.isArray(baseDir) && baseDir.indexOf(conf.paths.src) !== -1)) { 16 | routes = { 17 | '/bower_components': 'bower_components', 18 | '/.tmp': '.tmp' 19 | }; 20 | } 21 | 22 | var server = { 23 | baseDir: baseDir, 24 | routes: routes 25 | }; 26 | 27 | /* 28 | * You can add a proxy to your backend by uncommenting the line below. 29 | * You just have to configure a context which will we redirected and the target url. 30 | * Example: $http.get('/users') requests will be automatically proxified. 31 | * 32 | * For more details and option, https://github.com/chimurai/http-proxy-middleware/blob/v0.9.0/README.md 33 | */ 34 | browserSync.instance = browserSync.init({ 35 | startPath: '/', 36 | server: server, 37 | browser: browser 38 | }); 39 | } 40 | 41 | browserSync.use(browserSyncSpa({ 42 | selector: '[ng-app]'// Only needed for angular apps 43 | })); 44 | 45 | gulp.task('serve', ['watch'], function () { 46 | browserSyncInit([path.join(conf.paths.tmp, '/serve'), conf.paths.src]); 47 | }); 48 | 49 | gulp.task('serve:dist', ['build'], function () { 50 | browserSyncInit(conf.paths.dist); 51 | }); 52 | 53 | gulp.task('serve:e2e', ['inject'], function () { 54 | browserSyncInit([conf.paths.tmp + '/serve', conf.paths.src], []); 55 | }); 56 | 57 | gulp.task('serve:e2e-dist', ['build'], function () { 58 | browserSyncInit(conf.paths.dist, []); 59 | }); 60 | -------------------------------------------------------------------------------- /src/app/login/login.js: -------------------------------------------------------------------------------- 1 | (function (angular, smi2) { 2 | 'use strict'; 3 | 4 | angular.module(smi2.app.name).controller('LoginController', LoginController); 5 | LoginController.$inject = [ 6 | '$scope', 7 | '$state', 8 | 'localStorageService', 9 | 'API', 10 | '$mdToast', 11 | 'ThemeService' 12 | ]; 13 | 14 | /** 15 | * @ngdoc controller 16 | * @name smi2.controller:LoginController 17 | * @description Login page controller 18 | */ 19 | function LoginController($scope, $state, localStorageService, API, $mdToast, ThemeService) { 20 | 21 | const ALL_BASES_KEY = 'basesConfig'; 22 | 23 | $scope.vars = { 24 | bases: localStorageService.get(ALL_BASES_KEY) || [], 25 | db: {}, 26 | loading: false, 27 | version: smi2.app.version, 28 | themes: ThemeService.list 29 | }; 30 | 31 | /** 32 | * Login with saving connection settings 33 | */ 34 | $scope.login = () => { 35 | $scope.vars.loading = true; 36 | 37 | // Saving to LocalStorage 38 | if ($scope.vars.db.id) { 39 | for (var i = 0; i < $scope.vars.bases.length; i++) { 40 | if ($scope.vars.bases[i].id == $scope.vars.db.id) { 41 | $scope.vars.bases[i] = $scope.vars.db; 42 | break; 43 | } 44 | } 45 | } else { 46 | $scope.vars.db.id = (new Date()).getTime(); 47 | $scope.vars.bases.push($scope.vars.db); 48 | } 49 | localStorageService.set(ALL_BASES_KEY, $scope.vars.bases); 50 | 51 | API.setConnection($scope.vars.db); 52 | API.query('SELECT \'login success\'').then( 53 | () => $state.go('sql'), 54 | () => { 55 | $scope.vars.loading = false; 56 | $mdToast.show( 57 | $mdToast 58 | .simple() 59 | .content('Ошибка доступа') 60 | .theme(ThemeService.theme) 61 | .position('bottom right') 62 | ); 63 | } 64 | ); 65 | }; 66 | 67 | /** 68 | * Remove connection item 69 | */ 70 | $scope.remove = () => { 71 | const index = $scope.vars.bases.findIndex((item) => (item.id == $scope.vars.db.id)); 72 | $scope.vars.bases.splice(index); 73 | localStorageService.set(ALL_BASES_KEY, $scope.vars.bases); 74 | $scope.vars.db = {}; 75 | }; 76 | 77 | /** 78 | * Change UI theme 79 | * @param theme 80 | */ 81 | $scope.setUiTheme = (theme) => ThemeService.set(theme.name); 82 | } 83 | })(angular, smi2); 84 | -------------------------------------------------------------------------------- /src/app/index.route.js: -------------------------------------------------------------------------------- 1 | ((angular, smi2) => { 2 | 'use strict'; 3 | 4 | angular 5 | .module(smi2.app.name) 6 | .config(($stateProvider) => { 7 | 8 | $stateProvider 9 | 10 | // Base routing with auth detection 11 | .state('base', { 12 | abstract: true, 13 | resolve: { 14 | session: ['$q', 'API', ($q, API) => { 15 | var defer = $q.defer(); 16 | if (angular.isDefined(API.getConnectionInfo().host)) { 17 | defer.resolve(); 18 | } else { 19 | defer.reject('notAuthorized'); 20 | } 21 | return defer.promise; 22 | }] 23 | }, 24 | templateUrl: 'app/base/base.html' 25 | }) 26 | 27 | // Design route 28 | .state('layout', { 29 | parent: 'base', 30 | abstract: true, 31 | views: { 32 | header: { 33 | templateUrl: 'app/base/header.html', 34 | controller: 'HeaderController' 35 | }, 36 | sidebar: { 37 | templateUrl: 'app/base/sidebar.html', 38 | controller: 'SidebarController' 39 | }, 40 | main: { 41 | template: '' 42 | } 43 | } 44 | }) 45 | 46 | // Dashboard state. Now it's only redir to sql state 47 | .state('dashboard', { 48 | parent: 'layout', 49 | url: '/', 50 | controller: ['$state', $state => $state.go('sql')] 51 | }) 52 | 53 | // Login state 54 | .state('login', { 55 | url: '/login', 56 | templateUrl: 'app/login/login.html', 57 | controller: 'LoginController' 58 | }) 59 | 60 | // SQL state 61 | .state('sql', { 62 | parent: 'layout', 63 | url: '/sql', 64 | templateUrl: 'app/sql/sql.html', 65 | controller: 'SqlController' 66 | }) 67 | 68 | // Show 1 table 69 | .state('table', { 70 | parent: 'layout', 71 | url: '/database/{dbName}/table/{tableName}', 72 | templateUrl: 'app/table/table.html', 73 | controller: 'TableController' 74 | }) 75 | 76 | // 4040 not found handle 77 | .state('404', { 78 | parent: 'layout', 79 | templateUrl: 'app/base/404.html' 80 | }); 81 | }); 82 | })(angular, smi2); 83 | -------------------------------------------------------------------------------- /gulp/conf.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This file contains the variables used in other gulp files 3 | * which defines tasks 4 | * By design, we only put there very generic config values 5 | * which are used in several places to keep good readability 6 | * of the tasks 7 | */ 8 | 9 | var gutil = require('gulp-util'); 10 | 11 | /** 12 | * The main paths of your project handle these with care 13 | */ 14 | exports.paths = { 15 | src: 'src', 16 | dist: 'docs', 17 | tmp: '.tmp', 18 | e2e: 'e2e', 19 | docs: 'help' 20 | }; 21 | 22 | /** 23 | * Wiredep is the lib which inject bower dependencies in your project 24 | * Mainly used to inject script tags in the index.html but also used 25 | * to inject css preprocessor deps and js files in karma 26 | */ 27 | exports.wiredep = { 28 | exclude: [/\/bootstrap\.js$/, /\/bootstrap\.css/], 29 | directory: 'bower_components', 30 | overrides: { 31 | 'ace-builds': { 32 | main: [ 33 | 'src-min-noconflict/ace.js', 34 | 'src-min-noconflict/ext-language_tools.js', 35 | 'src-min-noconflict/ext-beautify.js', 36 | 'src-min-noconflict/ext-statusbar.js', 37 | 'src-min-noconflict/ext-settings_menu.js', 38 | 'src-min-noconflict/theme-ambiance.js', 39 | 'src-min-noconflict/theme-crimson_editor.js', 40 | 'src-min-noconflict/theme-iplastic.js', 41 | 'src-min-noconflict/theme-mono_industrial.js', 42 | 'src-min-noconflict/theme-terminal.js', 43 | 'src-min-noconflict/theme-tomorrow_night.js', 44 | 'src-min-noconflict/theme-chaos.js', 45 | 'src-min-noconflict/theme-dawn.js', 46 | 'src-min-noconflict/theme-katzenmilch.js', 47 | 'src-min-noconflict/theme-monokai.js', 48 | 'src-min-noconflict/theme-textmate.js', 49 | 'src-min-noconflict/theme-twilight.js', 50 | 'src-min-noconflict/theme-chrome.js', 51 | 'src-min-noconflict/theme-dreamweaver.js', 52 | 'src-min-noconflict/theme-kr_theme.js', 53 | 'src-min-noconflict/theme-pastel_on_dark.js', 54 | 'src-min-noconflict/theme-tomorrow.js', 55 | 'src-min-noconflict/theme-vibrant_ink.js', 56 | 'src-min-noconflict/theme-clouds.js', 57 | 'src-min-noconflict/theme-eclipse.js', 58 | 'src-min-noconflict/theme-kuroir.js', 59 | 'src-min-noconflict/theme-solarized_dark.js', 60 | 'src-min-noconflict/theme-tomorrow_night_blue.js', 61 | 'src-min-noconflict/theme-xcode.js', 62 | 'src-min-noconflict/theme-clouds_midnight.js', 63 | 'src-min-noconflict/theme-github.js', 64 | 'src-min-noconflict/theme-merbivore.js', 65 | 'src-min-noconflict/theme-solarized_light.js', 66 | 'src-min-noconflict/theme-tomorrow_night_bright.js', 67 | 'src-min-noconflict/theme-cobalt.js', 68 | 'src-min-noconflict/theme-idle_fingers.js', 69 | 'src-min-noconflict/theme-merbivore_soft.js', 70 | 'src-min-noconflict/theme-sqlserver.js', 71 | 'src-min-noconflict/theme-tomorrow_night_eighties.js' 72 | ] 73 | } 74 | } 75 | }; 76 | 77 | /** 78 | * Common implementation for an error handler of a Gulp plugin 79 | */ 80 | exports.errorHandler = function(title) { 81 | 'use strict'; 82 | 83 | return function(err) { 84 | gutil.log(gutil.colors.red('[' + title + ']'), err.toString()); 85 | this.emit('end'); 86 | }; 87 | }; 88 | -------------------------------------------------------------------------------- /src/app/base/sidebar.js: -------------------------------------------------------------------------------- 1 | (( angular, smi2 ) => { 2 | 'use strict'; 3 | 4 | angular.module( smi2.app.name ).controller( 'SidebarController', SidebarController ); 5 | SidebarController.$inject = [ 6 | '$scope', 7 | '$rootScope', 8 | '$state', 9 | 'API', 10 | 'ThemeService', 11 | '$mdSidenav' 12 | ]; 13 | 14 | /** 15 | * @ngdoc controller 16 | * @name smi2.controller:SidebarController 17 | * @description Контроллер бокового меню 18 | */ 19 | function SidebarController( $scope, $rootScope, $state, API, ThemeService, $mdSidenav ) { 20 | $scope.vars = { 21 | loaded: false, 22 | databases: [ ] 23 | }; 24 | 25 | $scope.vars.scrollConfig = { 26 | autoHideScrollbar: false, 27 | theme: ThemeService.isDark( ) 28 | ? 'light' 29 | : 'dark', 30 | scrollButtons: { 31 | enable: false 32 | }, 33 | scrollInertia: 400, 34 | advanced: { 35 | updateOnContentResize: true 36 | } 37 | }; 38 | 39 | $scope.vars.metis = { 40 | config: { 41 | toggle: true, 42 | preventDefault: false 43 | } 44 | }; 45 | 46 | $scope.clickAndSelect = ( database, event ) => { 47 | if ( database.name == $rootScope.currentDatabase ) { 48 | event.stopPropagation( ); 49 | return false; 50 | } 51 | $scope.selectDatabase( database ); 52 | }; 53 | 54 | /** 55 | * Select database 56 | */ 57 | $scope.selectDatabase = database => { 58 | $rootScope.currentDatabase = database.name; 59 | $mdSidenav( 'tableSiedenav' ).close( ); 60 | }; 61 | 62 | /** 63 | * Open table in right sidebar 64 | */ 65 | $scope.openTable = ( table ) => { 66 | $mdSidenav( 'tableSiedenav' ).close( ); 67 | $rootScope.currentTable = table.name; 68 | $mdSidenav( 'tableSiedenav' ).open( ); 69 | }; 70 | 71 | API.query( 'SELECT ' + 72 | ' database, ' + 73 | ' name ' + 74 | 'FROM ' + 75 | ' system.tables' ).then(res => { 76 | let data = res.data || [ ]; 77 | $scope.vars.databases = data.reduce(( prev, item ) => { 78 | for ( let a of prev ) { 79 | if ( a.name == item.database ) { 80 | a.tables.push({ name: item.name }); 81 | return prev; 82 | } 83 | } 84 | return [ 85 | ...prev, { 86 | name: item.database, 87 | tables: [ 88 | { 89 | name: item.name 90 | } 91 | ] 92 | } 93 | ]; 94 | }, [ ]); 95 | 96 | $scope.selectDatabase($scope.vars.databases[0]); 97 | $scope.vars.loaded = true; 98 | }); 99 | } 100 | })( angular, smi2 ); 101 | -------------------------------------------------------------------------------- /src/app/table/table-container.js: -------------------------------------------------------------------------------- 1 | (( angular, smi2 ) => { 2 | 'use strict'; 3 | 4 | angular.module( smi2.app.name ).controller( 'TableContainerController', TableController ); 5 | TableController.$inject = [ 6 | '$scope', 7 | '$rootScope', 8 | 'API', 9 | 'ThemeService', 10 | '$stateParams', 11 | '$mdSidenav', 12 | '$mdComponentRegistry' 13 | ]; 14 | 15 | /** 16 | * @ngdoc controller 17 | * @name smi2.controller:TableContainerController 18 | * @description Контроллер страницы 1 таблицы БД 19 | */ 20 | function TableController( $scope, $rootScope, API, ThemeService, $stateParams, $mdSidenav, $mdComponentRegistry ) { 21 | 22 | $scope.vars = { 23 | columns: {}, 24 | data: null, 25 | grid: null, 26 | limit: 100, 27 | offset: 0, 28 | statistics: {}, 29 | loading: false, 30 | scrollConfig: { 31 | autoHideScrollbar: false, 32 | theme: ThemeService.isDark( ) 33 | ? 'light' 34 | : 'dark', 35 | scrollButtons: { 36 | enable: false 37 | }, 38 | scrollInertia: 100, 39 | advanced: { 40 | updateOnContentResize: true 41 | } 42 | } 43 | }; 44 | $scope.initContainer = ( ) => { 45 | $rootScope.$watch('currentTable', ( val ) => { 46 | if ( val ) { 47 | $scope.vars.currentTable = $rootScope.currentTable; 48 | $scope.vars.currentDatabase = $rootScope.currentDatabase; 49 | $scope.init( ); 50 | } 51 | }); 52 | }; 53 | 54 | $scope.initOnGo = ( ) => { 55 | if ( $scope.$parent.vars ) { 56 | if ($mdComponentRegistry.get( 'tableSiedenav' )) { 57 | $mdSidenav( 'tableSiedenav' ).close( ); 58 | } 59 | $scope.vars.currentTable = $scope.$parent.vars.tableName; 60 | $scope.vars.currentDatabase = $scope.$parent.vars.dbName; 61 | $scope.init( ); 62 | 63 | } 64 | }; 65 | 66 | /** 67 | * Загрузка данных 68 | */ 69 | $scope.load = ( ) => { 70 | $scope.vars.data = -1; 71 | API.query( ` 72 | select * 73 | from ${ $scope.vars.currentDatabase }.${ $scope.vars.currentTable } 74 | limit ${ $scope.vars.offset }, ${ $scope.vars.limit } 75 | ` ).then( function ( data ) { 76 | $scope.vars.data = API.dataToHtml( data ); 77 | $scope.vars.loading = false; 78 | }, function ( response ) { 79 | $scope.vars.loading = false; 80 | console.error( 'Ошибка ' + response ); 81 | }); 82 | }; 83 | 84 | $scope.init = ( ) => { 85 | $scope.vars.loading = true; 86 | 87 | /** 88 | * Запрос полей таблицы 89 | */ 90 | API.query( 'describe table ' + $scope.vars.currentDatabase + '.' + $scope.vars.currentTable ).then( data => $scope.vars.columns = data ); 91 | 92 | /** 93 | * Запрос статистики по таблице 94 | */ 95 | API.query( 'SELECT ' + 96 | ' table, ' + 97 | ' formatReadableSize(sum(bytes)) as size, ' + 98 | ' sum(bytes) as sizeBytes, ' + 99 | ' min(min_date) as minDate, ' + 100 | ' max(max_date) as maxDate ' + 101 | 'FROM ' + 102 | ' system.parts ' + 103 | 'WHERE ' + 104 | ' database = \'' + $scope.vars.currentDatabase + '\' AND ' + ' ( ' + ' table = \'' + $scope.vars.currentTable + '\' OR ' + ' table = \'' + $scope.vars.currentTable + '_sharded\'' + ' ) ' + 'GROUP BY ' + ' table ' ).then(response => $scope.vars.statistics = (response && response.data.length && response.data[0]) || {}); 105 | 106 | $scope.load( ); 107 | }; 108 | 109 | /** 110 | * Шаг вперед по данным 111 | */ 112 | $scope.loadNext = ( ) => { 113 | $scope.vars.offset += $scope.vars.limit; 114 | $scope.load( ); 115 | }; 116 | 117 | /** 118 | * Шаг назад по данным 119 | */ 120 | $scope.loadPrev = ( ) => { 121 | if ( $scope.vars.offset > 0 ) { 122 | $scope.vars.offset -= $scope.vars.limit; 123 | $scope.load( ); 124 | } 125 | }; 126 | } 127 | })( angular, smi2 ); 128 | -------------------------------------------------------------------------------- /gulp/build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var gulp = require('gulp'); 5 | var conf = require('./conf'); 6 | var packageJson = require('../package.json'); 7 | 8 | var $ = require('gulp-load-plugins')({ 9 | pattern: ['gulp-*', 'main-bower-files', 'uglify-save-license', 'del'] 10 | }); 11 | 12 | gulp.task('partials', function () { 13 | return gulp.src([ 14 | path.join(conf.paths.src, '/app/**/*.html'), 15 | path.join(conf.paths.src, '/app/**/*.json'), 16 | path.join(conf.paths.tmp, '/serve/app/**/*.html'), 17 | path.join(conf.paths.tmp, '/serve/app/**/*.json') 18 | ]) 19 | .pipe($.minifyHtml({ 20 | empty: true, 21 | spare: true, 22 | quotes: true 23 | })) 24 | .pipe($.angularTemplatecache('templateCacheHtml.js', { 25 | module: 'SMI2', 26 | root: 'app' 27 | })) 28 | .pipe(gulp.dest(conf.paths.tmp + '/partials/')); 29 | }); 30 | 31 | gulp.task('html', ['inject', 'partials'], function () { 32 | var partialsInjectFile = gulp.src([ 33 | path.join(conf.paths.src, '/app/**/*.app.js'), 34 | path.join(conf.paths.tmp, '/partials/templateCacheHtml.js') 35 | ], { 36 | read: false 37 | }); 38 | var partialsInjectOptions = { 39 | starttag: '', 40 | ignorePath: path.join(conf.paths.tmp, '/partials'), 41 | addRootSlash: false 42 | }; 43 | 44 | var htmlFilter = $.filter('*.html', { 45 | restore: true 46 | }); 47 | var jsFilter = $.filter('**/*.js', { 48 | restore: true 49 | }); 50 | var jsVendor = $.filter(['**/*.js', '!**/vendor-*.js'], { 51 | restore: true 52 | }); 53 | var cssFilter = $.filter('**/*.css', { 54 | restore: true 55 | }); 56 | var assets; 57 | 58 | return gulp.src(path.join(conf.paths.tmp, '/serve/*.html')) 59 | .pipe($.inject(partialsInjectFile, partialsInjectOptions)) 60 | .pipe(assets = $.useref.assets()) 61 | .pipe($.rev()) 62 | .pipe(jsFilter) 63 | .pipe(jsVendor) 64 | .pipe($.babel({ 65 | presets: ['es2015', 'stage-0'] 66 | })) 67 | .on('error', conf.errorHandler('babel')) 68 | .pipe(jsVendor.restore) 69 | //.pipe($.replace('assets/', 'app/assets/')) 70 | //.pipe($.sourcemaps.init()) 71 | .pipe($.ngAnnotate()) 72 | .pipe($.uglify({ 73 | preserveComments: $.uglifySaveLicense 74 | })).on('error', conf.errorHandler('Uglify')) 75 | //.pipe($.sourcemaps.write('maps')) 76 | .pipe(jsFilter.restore) 77 | .pipe(cssFilter) 78 | .pipe($.replace('url(./fonts/', 'url(../fonts/')) // костылек для lumx 79 | //.pipe($.sourcemaps.init()) 80 | //для favicon production 81 | .pipe($.minifyCss({ 82 | processImport: false 83 | })) 84 | //.pipe($.sourcemaps.write('maps')) 85 | .pipe(cssFilter.restore) 86 | .pipe(assets.restore()) 87 | .pipe($.useref()) 88 | .pipe($.revReplace()) 89 | .pipe(htmlFilter) 90 | .pipe($.replace('', '')) 91 | .pipe($.minifyHtml({ 92 | empty: true, 93 | spare: true, 94 | quotes: true, 95 | conditionals: true 96 | })) 97 | .pipe(htmlFilter.restore) 98 | .pipe(gulp.dest(path.join(conf.paths.dist, '/'))) 99 | .pipe($.size({ 100 | title: path.join(conf.paths.dist, '/'), 101 | showFiles: true 102 | })); 103 | }); 104 | 105 | // Only applies for fonts from bower dependencies 106 | // Custom fonts are handled by the "other" task 107 | gulp.task('fonts', function () { 108 | return gulp.src($.mainBowerFiles()) 109 | .pipe($.filter('**/*.{eot,svg,ttf,woff,woff2}')) 110 | .pipe($.flatten()) 111 | .pipe(gulp.dest(path.join(conf.paths.dist, '/fonts/'))); 112 | }); 113 | 114 | gulp.task('other', function () { 115 | var fileFilter = $.filter(function (file) { 116 | return file.stat.isFile(); 117 | }); 118 | 119 | return gulp.src([ 120 | path.join(conf.paths.src, '/**/*'), 121 | path.join('!' + conf.paths.src, '/**/*.{html,css,js,less,json}') 122 | ]) 123 | .pipe(fileFilter) 124 | .pipe(gulp.dest(path.join(conf.paths.dist, '/'))); 125 | }); 126 | 127 | gulp.task('clean', function () { 128 | return $.del([path.join(conf.paths.dist, '/'), path.join(conf.paths.tmp, '/')]); 129 | }); 130 | 131 | gulp.task('cname', function () { 132 | return gulp.src('./assets/*').pipe(gulp.dest(path.join(conf.paths.dist, '/'))); 133 | }); 134 | 135 | gulp.task('build', ['html', 'fonts', 'other', 'cname']); 136 | -------------------------------------------------------------------------------- /src/app/index.config.js: -------------------------------------------------------------------------------- 1 | ((angular, smi2) => { 2 | 'use strict'; 3 | 4 | 5 | /** 6 | * Providers configuration 7 | */ 8 | angular 9 | .module(smi2.app.name) 10 | .config([ 11 | '$locationProvider', 12 | '$httpProvider', 13 | '$sceProvider', 14 | '$urlRouterProvider', 15 | '$mdThemingProvider', 16 | 'ThemeServiceProvider', 17 | function ($locationProvider, 18 | $httpProvider, 19 | $sceProvider, 20 | $urlRouterProvider, 21 | $mdThemingProvider, 22 | ThemeService) { 23 | 24 | // Запуск HTML5 режима HISTORY API, без решетки 25 | $locationProvider.html5Mode(true).hashPrefix('!'); 26 | 27 | // Проверка авторизации в httpInterceptor 28 | $httpProvider.interceptors.push('HttpInterceptor'); 29 | 30 | // Разрешаю ng-bind-html 31 | $sceProvider.enabled(false); 32 | 33 | // Если state не найден - шлю 404 34 | $urlRouterProvider.otherwise(function ($injector) { 35 | var $state = $injector.get("$state"); 36 | $state.transitionTo('404'); 37 | }); 38 | 39 | if (ThemeService.$get().isDark()) { 40 | $mdThemingProvider 41 | .theme('default') 42 | .dark() 43 | .primaryPalette('blue') 44 | .accentPalette('blue', { 45 | 'default': '500' 46 | }); 47 | } 48 | } 49 | ]) 50 | .config(['$translateProvider', function ($translateProvider) { 51 | $translateProvider.translations('en', { 52 | 'Подключение': 'Connection', 53 | 'Неверные данные': 'some data is wrong', 54 | 'Название': 'name', 55 | 'Хост:порт': 'host:port', 56 | 'Логин': 'login', 57 | 'Пароль': 'password', 58 | 'Удалить': 'Delete', 59 | 'Войти': 'Sign in', 60 | 'не найдено': 'not found', 61 | 'нет данных': 'no data', 62 | 'Назад': 'Back', 63 | 'Выход': 'Sing out', 64 | 'Шрифт': 'font', 65 | 'История запросов пуста': 'Requests\'s history is empty', 66 | 'Структура': 'Structure', 67 | 'Данные': 'Data', 68 | 'Информация': 'Information', 69 | 'Размер': 'Size', 70 | 'Размер, байт': 'Size in bytes', 71 | 'Первая запись': 'First record', 72 | 'Последняя запись': 'Last record', 73 | 'Тип': 'Type', 74 | 'Default тип': 'Default type', 75 | 'Значение': 'Default value', 76 | 'Рабочий стол': 'Dashboard', 77 | 'База': 'Database', 78 | 'Выполнить ⌘ + ⏎': 'Run ⌘ + ⏎', 79 | 'Таблица': 'Table', 80 | 'Таблица ': 'Table ', 81 | 'Хотите покинуть страницу?': 'Do you want to leave this page?', 82 | 'Ошибка': 'Error', 83 | 'Не введен SQL': 'SQL request is empty', 84 | 'Выполнить выделенное ⌘ + ⏎': 'Run selected ⌘ + ⏎', 85 | 'Выполнить все ⇧ + ⌘ + ⏎': 'Run all ⇧ + ⌘ + ⏎', 86 | 'Просмотр': 'Preview', 87 | 'Ошибка ': 'Error ', 88 | 'Сохранить':'Save', 89 | 'Лог запросов': 'Request\'s log', 90 | 'Тема интерфейса': 'Theme', 91 | 'Тема': 'Theme', 92 | 'Максимум строк': 'Maximum lines', 93 | 'Настройки редактора': 'Editor settings', 94 | 'Закрыть': 'Close', 95 | 'Размер шрифта': 'Font size', 96 | 'Формат результатов': 'Result\'s format', 97 | 'Тема редактора':'Editor theme', 98 | 'Сохранять сессию': 'Save session', 99 | 'На весь экран': 'Full screen', 100 | 'Словари': 'Dictionaries', 101 | 'CSV с заголовком': 'CSV with headers', 102 | 'CSV без заголовка': 'CSV without headers', 103 | 'Табулированный текст с заголовками': 'Tabulated text with headers', 104 | 'Табулированный текст без заголовков': 'Tabulated text without headers', 105 | 'время':'time', 106 | 'строк прочитано':'lines read', 107 | 'прочитано':'read', 108 | 'SQL изменен. Сохранить перед закрытием?': 'SQL was changed. Save it before exit?', 109 | 'Да':'Yes', 110 | 'Нет': 'No', 111 | 'Светлая тема': 'Light theme', 112 | 'Темная тема': 'Dark theme', 113 | 'с': 'from', 114 | 'по': 'to', 115 | 'Открыть таблицу': 'Open table' 116 | }) 117 | .translations('ru', { 118 | }) 119 | .registerAvailableLanguageKeys(['en', 'ru'], { 120 | 'ru_*': 'ru', 121 | 'ru-*': 'ru', 122 | '*': 'en', 123 | }) 124 | .determinePreferredLanguage(); 125 | // this line used for translation check 126 | //$translateProvider.useMissingTranslationHandlerLog(); 127 | 128 | }]); 129 | })(angular, smi2); 130 | -------------------------------------------------------------------------------- /src/app/login/login.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | 5 |
6 |
7 | 8 |
9 |
10 | Clickhouse 11 |
12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | {{theme.title}} 21 | 22 | 23 | 24 |
25 |
26 |
27 |
28 |
29 | 44 | 45 | 106 |
107 | 108 |
109 | -------------------------------------------------------------------------------- /src/app/table/table-container.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

{{vars.currentDatabase}} 4 | 5 | {{vars.currentTable}}

6 | 7 | 8 | 9 | 10 | 11 | {{'Структура' | translate}} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 30 | 31 | 32 |
{{'Название' | translate}}{{'Тип' | translate}}{{'Default тип' | translate}}{{'Значение' | translate}}
{{$index + 1}} 28 | {{field[key]}} 29 |
33 |
34 |
35 | 36 | 37 | 38 | 39 | {{'Данные' | translate}} 40 | 41 | 42 |
43 | 44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 |
52 | {{'Данные' | translate}} 53 | {{'с' | translate}} 54 | {{vars.offset}} 55 | {{'по' | translate}} 56 | {{vars.offset + vars.limit}} 57 |
58 |
59 | {{'нет данных' | translate}} 60 | 61 |
62 |
63 |
64 |
65 | 66 | 67 | 68 | 69 | {{'Информация' | translate}} 70 | 71 |
72 | 73 | 74 | 75 | 76 | 77 | 80 | 81 | 82 | 83 | 84 | 87 | 88 | 89 | 90 | 91 | 94 | 95 | 96 | 97 | 98 | 101 | 102 | 103 |
1{{'Размер' | translate}} 78 | {{vars.statistics.size}} 79 |
2{{'Размер, байт' | translate}} 85 | {{vars.statistics.sizeBytes}} 86 |
3{{'Первая запись' | translate}} 92 | {{vars.statistics.minDate}} 93 |
4{{'Последняя запись' | translate}} 99 | {{vars.statistics.maxDate}} 100 |
104 |
105 |
106 |
107 |
108 |
109 | -------------------------------------------------------------------------------- /src/less/layout.less: -------------------------------------------------------------------------------- 1 | .breadcrumb { 2 | list-style: none; 3 | margin: 0; 4 | } 5 | 6 | .breadcrumb li { 7 | display: inline-block; 8 | } 9 | 10 | body { 11 | font-family: Helvetica, arial; 12 | } 13 | 14 | body .wrapper { 15 | display: -webkit-box; 16 | display: -ms-flexbox; 17 | display: flex; 18 | -webkit-box-orient: vertical; 19 | -webkit-box-direction: normal; 20 | -ms-flex-direction: column; 21 | flex-direction: column; 22 | min-height: 100vh; 23 | } 24 | 25 | .content { 26 | &.login{ 27 | width: 100%; 28 | .main-view{ 29 | justify-content: center; 30 | } 31 | } 32 | display: flex; 33 | height: 100vh; 34 | } 35 | .main-content.login .footer{ 36 | position: fixed; 37 | bottom: 0; 38 | left: 0; 39 | right: 0; 40 | } 41 | @media only screen and (min-width: 0px) and (max-width: 600px) { 42 | .content { 43 | margin-top: 56px; 44 | } 45 | } 46 | 47 | .content .main-content { 48 | flex: 1; 49 | display: flex; 50 | flex-flow: column nowrap; 51 | overflow: auto; 52 | margin-top: 40px; 53 | position: relative; 54 | } 55 | 56 | .content .main-content .main-view { 57 | -webkit-box-flex: 1; 58 | -ms-flex: 1; 59 | flex: 1; 60 | display: -webkit-box; 61 | display: -ms-flexbox; 62 | display: flex; 63 | -webkit-box-pack: center; 64 | -ms-flex-pack: center; 65 | justify-content: center; 66 | -webkit-box-align: center; 67 | -ms-flex-align: center; 68 | align-items: center; 69 | } 70 | 71 | md-sidenav[md-component-id="tableSiedenav"] { 72 | width: 50% !important; 73 | max-width: 50% !important; 74 | height: 100%; 75 | overflow: hidden; 76 | .right-button{ 77 | position: absolute; 78 | right: 20px; 79 | top: 20px; 80 | z-index: 1; 81 | } 82 | 83 | .md-closed.md-locked-open-add-active, 84 | .md-locked-open { 85 | width: 50% !important; 86 | height: 100%; 87 | max-width: 50% !important; 88 | } 89 | 90 | md-content { 91 | position: absolute; 92 | top: 0; 93 | bottom: 0; 94 | width: 100%; 95 | overflow: hidden; 96 | padding-top: 0; 97 | padding-bottom: 0; 98 | 99 | md-tab-content { 100 | max-height: 100%; 101 | 102 | > div { 103 | max-height: 100%; 104 | } 105 | } 106 | 107 | .mCustomScrollbar { 108 | // height: 100%; 109 | } 110 | } 111 | } 112 | @media only screen and (min-width: 0px) and (max-width: 600px) { 113 | .content .main-content .main-view { 114 | margin-left: 8px; 115 | margin-right: 8px; 116 | } 117 | } 118 | 119 | .content .main-content .main-view.ng-enter { 120 | -webkit-animation: bounceIn 0.8s; 121 | animation: bounceIn 0.8s; 122 | } 123 | 124 | .content .main-content .main-view .full-width { 125 | -ms-flex-item-align: start; 126 | align-self: flex-start; 127 | padding: 10px 30px 20px; 128 | width: 100%; 129 | } 130 | 131 | .footer .copyright { 132 | margin: auto; 133 | text-shadow: 1px 1px rgba(0, 0, 0, 0.2); 134 | } 135 | 136 | .footer .toolbar { 137 | background: #424242 !important; 138 | } 139 | 140 | .header-view { 141 | position: fixed; 142 | width: 100%; 143 | z-index: 2; 144 | } 145 | @media only screen and (min-width: 0px) and (max-width: 600px) { 146 | .header-view .toolbar { 147 | padding-left: 0; 148 | padding-right: 0; 149 | } 150 | } 151 | 152 | .header-view .toolbar .fs-title a { 153 | text-decoration: none; 154 | } 155 | 156 | body #loading-bar .bar { 157 | background: #2c3e50; 158 | height: 3px; 159 | } 160 | 161 | body #loading-bar .peg { 162 | box-shadow: 0 0 10px #2c3e50, 0 0 5px #2c3e50; 163 | } 164 | 165 | body #loading-bar-spinner .spinner-icon { 166 | border-top-color: #2c3e50; 167 | border-left-color: #2c3e50; 168 | } 169 | 170 | .sidebar-view { 171 | border-right: 1px solid rgba(0, 0, 0, 0.2); 172 | } 173 | @media only screen and (min-width: 0px) and (max-width: 600px) { 174 | .sidebar-view { 175 | position: fixed; 176 | height: 100%; 177 | width: 200px; 178 | box-shadow: 3px 0 6px rgba(0, 0, 0, 0.2); 179 | -webkit-transform: translateX(-200px); 180 | transform: translateX(-200px); 181 | -webkit-transition: box-shadow, -webkit-transform 0.6s cubic-bezier(0.23, 1, 0.32, 1); 182 | transition: box-shadow, transform 0.6s cubic-bezier(0.23, 1, 0.32, 1); 183 | z-index: 999; 184 | } 185 | 186 | .sidebar-view.show { 187 | -webkit-transform: none; 188 | transform: none; 189 | } 190 | } 191 | .sidebar-view .sidebar .sidebar-menu { 192 | list-style: none; 193 | } 194 | 195 | .sidebar-view .sidebar .sidebar-menu .menu-item { 196 | border-bottom: 1px solid rgba(0, 0, 0, 0.2); 197 | } 198 | 199 | .sidebar-view .sidebar .sidebar-menu .menu-item.active { 200 | border-radius: 5px; 201 | background-color: rgba(146, 205, 255, 0.42); 202 | } 203 | 204 | .sidebar-view .sidebar .sidebar-menu .menu-item.active i { 205 | color: #4caf50; 206 | } 207 | 208 | .sidebar-view .sidebar .sidebar-menu .menu-item.active:hover { 209 | background-color: rgba(146, 205, 255, 0.42); 210 | } 211 | 212 | .sidebar-view .sidebar .sidebar-menu .menu-item:hover { 213 | text-shadow: 1px 1px rgba(0, 0, 0, 0.2); 214 | background-color: #eee; 215 | } 216 | 217 | .sidebar-view .sidebar .sidebar-menu .menu-item .link { 218 | display: block; 219 | text-decoration: none; 220 | } 221 | 222 | .sidebar-view .sidebar .sidebar-menu .menu-item .link .text { 223 | line-height: 32px; 224 | font-size: 14pt; 225 | } 226 | @media only screen and (min-width: 0px) and (max-width: 600px) { 227 | .sidebar-view .sidebar-backdrop { 228 | position: absolute; 229 | top: 0; 230 | left: 200px; 231 | z-index: 990; 232 | background-color: rgba(0, 0, 0, 0.5); 233 | height: 100%; 234 | width: 100%; 235 | } 236 | } 237 | 238 | .menu-button { 239 | width: 180px; 240 | text-align: left; 241 | margin: 0; 242 | padding: 8px; 243 | } 244 | 245 | .menu-button__title { 246 | font-size: 16px; 247 | line-height: 18px; 248 | } 249 | 250 | .menu-button__subtitle { 251 | display: block; 252 | font-size: 10px; 253 | line-height: 12px; 254 | margin-bottom: 8px; 255 | } 256 | 257 | .header { 258 | height: 40px; 259 | line-height: 40px; 260 | 261 | md-card { 262 | height: 40px; 263 | line-height: 40px; 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /src/app/services/api.js: -------------------------------------------------------------------------------- 1 | ((angular, smi2) => { 2 | 'use strict'; 3 | 4 | angular.module(smi2.app.name).service('API', API); 5 | API.$inject = ['$http', '$q', 'localStorageService', '$sanitize', 'ThemeService']; 6 | 7 | /** 8 | * @ngdoc service 9 | * @name smi2.service:API 10 | * @description API manager 11 | */ 12 | function API($http, $q, localStorageService, $sanitize, ThemeService) { 13 | 14 | const CURRENT_BASE_KEY = 'currentBaseConfig'; 15 | const DEFAULT_PORT = 8123; 16 | 17 | let database = null; 18 | let connection = {}; 19 | 20 | // Первичная загрузка данных из LS 21 | let data = localStorageService.get(CURRENT_BASE_KEY); 22 | if (data && data.host) { 23 | connection = data; 24 | } 25 | 26 | /** 27 | * @ngdoc method 28 | * @methodOf smi2.service:API 29 | * @name setConnection 30 | * @description Сохранение данных подключения в БД 31 | * @param {mixed} db Данные подключения 32 | */ 33 | this.setConnection = (db) => { 34 | localStorageService.set(CURRENT_BASE_KEY, db); 35 | connection = angular.copy(db); 36 | 37 | // Check port 38 | if (!/.*\:\d+$/.test(connection.host)) { 39 | connection.host += ':' + DEFAULT_PORT; 40 | } 41 | }; 42 | 43 | /** 44 | * @ngdoc method 45 | * @methodOf smi2.service:API 46 | * @name clear 47 | * @description Сброс авторизации 48 | */ 49 | this.clear = () => { 50 | database = null; 51 | connection = {}; 52 | localStorageService.set(CURRENT_BASE_KEY, {}); 53 | }; 54 | 55 | /** 56 | * @ngdoc method 57 | * @methodOf smi2.service:API 58 | * @name query 59 | * @description Выполнение запроса к БД 60 | * @param {string} sql Текст запроса 61 | * @return {promise} Promise 62 | */ 63 | this.query = (sql, format, withDatabase, extend_settings) => { 64 | let defer = $q.defer(); 65 | let query = ''; 66 | 67 | if (format !== false) { 68 | format = (format || ' FoRmAt JSON'); 69 | if (format == 'null') { 70 | format = ''; 71 | } 72 | query = sql + ' ' + format; 73 | } else { 74 | query = sql; 75 | } 76 | var httpProto = ''; 77 | if (!(connection.host.indexOf('://') > 0 || connection.host.indexOf('/') == 0)) { 78 | httpProto = 'http://'; 79 | } 80 | let url = httpProto + connection.host + 81 | '/?query=' + encodeURIComponent(query); 82 | if (connection.login) { 83 | url += '&user=' + connection.login; 84 | } 85 | if (connection.password) { 86 | url += '&password=' + connection.password; 87 | } 88 | if (connection.host.indexOf('/') != 0) { 89 | url += '&add_http_cors_header=1'; 90 | } 91 | if (withDatabase) { 92 | url += '&database=' + database; 93 | } 94 | if (extend_settings) { 95 | url += '&' + extend_settings; 96 | } 97 | // console.info(query);// Не удалять не только для DEBUG. 98 | // Бебебе удалил 99 | 100 | let req = { 101 | method: (format ? 'GET' : 'POST'), // if not set format use POST 102 | url: url, 103 | transformResponse: (data, header, status) => { 104 | try { 105 | return angular.fromJson(data); 106 | } catch (err) { 107 | return (data ? data : "\nStatus:" + status + "\nHeaders:" + angular.toJson(header())); 108 | } 109 | } 110 | }; 111 | 112 | $http(req).then((response) => { 113 | defer.resolve(response.data); 114 | }, (response) => { 115 | defer.reject(response.data); 116 | }); 117 | 118 | return defer.promise; 119 | }; 120 | 121 | /** 122 | * @ngdoc method 123 | * @methodOf smi2.service:API 124 | * @name getConnectionInfo 125 | * @description Получение данных подключения 126 | * @return {mixed} Объект с данными подключения 127 | */ 128 | this.getConnectionInfo = () => connection; 129 | 130 | /** 131 | * @ngdoc method 132 | * @methodOf smi2.service:API 133 | * @name setDatabase 134 | * @description Установка дефолтной БД 135 | * @param {mixed} db Данные БД 136 | */ 137 | this.setDatabase = (db) => (database = db); 138 | 139 | /** 140 | * @ngdoc method 141 | * @methodOf smi2.service:API 142 | * @name getDatabase 143 | * @description Get database info 144 | */ 145 | this.getDatabase = () => database; 146 | 147 | /** 148 | * 149 | * @param data 150 | * @returns {string} 151 | */ 152 | this.dataToCreateTable = (data) => { 153 | let q = "\n" + 'CREATE TABLE x (' + "\n"; 154 | let keys = []; 155 | data.meta.forEach((cell) => keys.push("\t" + cell.name + " " + cell.type)); 156 | 157 | return q + keys.join(",\n") + "\n ) ENGINE = Log \n;;\n"; 158 | }; 159 | 160 | /** 161 | * @ngdoc method 162 | * @methodOf smi2.service:API 163 | * @name dataToHtml 164 | * @description Преобразование JSON данных запроса в таблицу 165 | * @param {mixed} data Объект, который содержит ответ БД 166 | * @return {string} Строка HTML 167 | */ 168 | this.dataToHtml = (data) => { 169 | 170 | let html = ``; 171 | let keys = []; 172 | data.meta.forEach((cell) => { 173 | html += ``; 176 | keys.push(cell.name); 177 | }); 178 | data.data.forEach((row) => { 179 | html += ''; 180 | keys.forEach((key) => { 181 | html += ''; 182 | }); 183 | html += ''; 184 | }); 185 | html += '
${$sanitize(cell.name)} 174 |
${$sanitize(cell.type)}
175 |
' + $sanitize(row[key]) + '
'; 186 | return html; 187 | }; 188 | 189 | 190 | this.dataToUIGrid = (data) => { 191 | 192 | let columnDefs = []; 193 | data.meta.forEach((cell) => { 194 | columnDefs.push( 195 | // pinnedLeft:true , width: 250, enablePinning:false ,pinnedRight:true 196 | {field: cell.name, minWidth: 100, enableColumnResizing: true, headerTooltip: cell.type} 197 | ); 198 | }); 199 | 200 | return { 201 | enableSorting: true, 202 | enableFiltering: true, 203 | enableColumnResizing: true, 204 | columnDefs: columnDefs, 205 | enableGridMenu: true, 206 | enableSelectAll: true, 207 | showGridFooter: true, 208 | showColumnFooter: true, 209 | data: data.data 210 | }; 211 | }; 212 | } 213 | })(angular, smi2); 214 | -------------------------------------------------------------------------------- /docs/fonts/ui-grid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright (C) 2017 by original authors @ fontello.com 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/less/core.less: -------------------------------------------------------------------------------- 1 | .width-100 { 2 | width: 100%; 3 | } 4 | 5 | .width-50 { 6 | width: 50%; 7 | } 8 | 9 | .pull-right { 10 | float: right; 11 | } 12 | 13 | .pull-left { 14 | float: left; 15 | } 16 | 17 | .text-center { 18 | text-align: center; 19 | } 20 | 21 | .m { 22 | margin: 8px !important; 23 | } 24 | 25 | .mt { 26 | margin-top: 8px !important; 27 | } 28 | 29 | .mr { 30 | margin-right: 8px !important; 31 | } 32 | 33 | .mb { 34 | margin-bottom: 8px !important; 35 | } 36 | 37 | .ml { 38 | margin-left: 8px !important; 39 | } 40 | 41 | .mh { 42 | margin-right: 8px !important; 43 | margin-left: 8px !important; 44 | } 45 | 46 | .mv { 47 | margin-top: 8px !important; 48 | margin-bottom: 8px !important; 49 | } 50 | 51 | .m- { 52 | margin: 4px !important; 53 | } 54 | 55 | .mt- { 56 | margin-top: 4px !important; 57 | } 58 | 59 | .mr- { 60 | margin-right: 4px !important; 61 | } 62 | 63 | .mb- { 64 | margin-bottom: 4px !important; 65 | } 66 | 67 | .ml- { 68 | margin-left: 4px !important; 69 | } 70 | 71 | .mh- { 72 | margin-right: 4px !important; 73 | margin-left: 4px !important; 74 | } 75 | 76 | .mv- { 77 | margin-top: 4px !important; 78 | margin-bottom: 4px !important; 79 | } 80 | 81 | .m\+ { 82 | margin: 16px !important; 83 | } 84 | 85 | .mt\+ { 86 | margin-top: 16px !important; 87 | } 88 | 89 | .mr\+ { 90 | margin-right: 16px !important; 91 | } 92 | 93 | .mb\+ { 94 | margin-bottom: 16px !important; 95 | } 96 | 97 | .ml\+ { 98 | margin-left: 16px !important; 99 | } 100 | 101 | .mh\+ { 102 | margin-right: 16px !important; 103 | margin-left: 16px !important; 104 | } 105 | 106 | .mv\+ { 107 | margin-top: 16px !important; 108 | margin-bottom: 16px !important; 109 | } 110 | 111 | .m\+\+ { 112 | margin: 24px !important; 113 | } 114 | 115 | .mt\+\+ { 116 | margin-top: 24px !important; 117 | } 118 | 119 | .mr\+\+ { 120 | margin-right: 24px !important; 121 | } 122 | 123 | .mb\+\+ { 124 | margin-bottom: 24px !important; 125 | } 126 | 127 | .ml\+\+ { 128 | margin-left: 24px !important; 129 | } 130 | 131 | .mh\+\+ { 132 | margin-right: 24px !important; 133 | margin-left: 24px !important; 134 | } 135 | 136 | .mv\+\+ { 137 | margin-top: 24px !important; 138 | margin-bottom: 24px !important; 139 | } 140 | 141 | .m\+\+\+ { 142 | margin: 32px !important; 143 | } 144 | 145 | .mt\+\+\+ { 146 | margin-top: 32px !important; 147 | } 148 | 149 | .mr\+\+\+ { 150 | margin-right: 32px !important; 151 | } 152 | 153 | .mb\+\+\+ { 154 | margin-bottom: 32px !important; 155 | } 156 | 157 | .ml\+\+\+ { 158 | margin-left: 32px !important; 159 | } 160 | 161 | .mh\+\+\+ { 162 | margin-right: 32px !important; 163 | margin-left: 32px !important; 164 | } 165 | 166 | .mv\+\+\+ { 167 | margin-top: 32px !important; 168 | margin-bottom: 32px !important; 169 | } 170 | 171 | .m0 { 172 | margin: 0 !important; 173 | } 174 | 175 | .mt0 { 176 | margin-top: 0 !important; 177 | } 178 | 179 | .mr0 { 180 | margin-right: 0 !important; 181 | } 182 | 183 | .mb0 { 184 | margin-bottom: 0 !important; 185 | } 186 | 187 | .ml0 { 188 | margin-left: 0 !important; 189 | } 190 | 191 | .mh0 { 192 | margin-right: 0 !important; 193 | margin-left: 0 !important; 194 | } 195 | 196 | .mv0 { 197 | margin-top: 0 !important; 198 | margin-bottom: 0 !important; 199 | } 200 | 201 | .p { 202 | padding: 8px !important; 203 | } 204 | 205 | .pt { 206 | padding-top: 8px !important; 207 | } 208 | 209 | .pr { 210 | padding-right: 8px !important; 211 | } 212 | 213 | .pb { 214 | padding-bottom: 8px !important; 215 | } 216 | 217 | .pl { 218 | padding-left: 8px !important; 219 | } 220 | 221 | .ph { 222 | padding-right: 8px !important; 223 | padding-left: 8px !important; 224 | } 225 | 226 | .pv { 227 | padding-top: 8px !important; 228 | padding-bottom: 8px !important; 229 | } 230 | 231 | .p- { 232 | padding: 4px !important; 233 | } 234 | 235 | .pt- { 236 | padding-top: 4px !important; 237 | } 238 | 239 | .pr- { 240 | padding-right: 4px !important; 241 | } 242 | 243 | .pb- { 244 | padding-bottom: 4px !important; 245 | } 246 | 247 | .pl- { 248 | padding-left: 4px !important; 249 | } 250 | 251 | .ph- { 252 | padding-right: 4px !important; 253 | padding-left: 4px !important; 254 | } 255 | 256 | .pv- { 257 | padding-top: 4px !important; 258 | padding-bottom: 4px !important; 259 | } 260 | 261 | .p\+ { 262 | padding: 16px !important; 263 | } 264 | 265 | .pt\+ { 266 | padding-top: 16px !important; 267 | } 268 | 269 | .pr\+ { 270 | padding-right: 16px !important; 271 | } 272 | 273 | .pb\+ { 274 | padding-bottom: 16px !important; 275 | } 276 | 277 | .pl\+ { 278 | padding-left: 16px !important; 279 | } 280 | 281 | .ph\+ { 282 | padding-right: 16px !important; 283 | padding-left: 16px !important; 284 | } 285 | 286 | .pv\+ { 287 | padding-top: 16px !important; 288 | padding-bottom: 16px !important; 289 | } 290 | 291 | .p\+\+ { 292 | padding: 24px !important; 293 | } 294 | 295 | .pt\+\+ { 296 | padding-top: 24px !important; 297 | } 298 | 299 | .pr\+\+ { 300 | padding-right: 24px !important; 301 | } 302 | 303 | .pb\+\+ { 304 | padding-bottom: 24px !important; 305 | } 306 | 307 | .pl\+\+ { 308 | padding-left: 24px !important; 309 | } 310 | 311 | .ph\+\+ { 312 | padding-right: 24px !important; 313 | padding-left: 24px !important; 314 | } 315 | 316 | .pv\+\+ { 317 | padding-top: 24px !important; 318 | padding-bottom: 24px !important; 319 | } 320 | 321 | .p\+\+\+ { 322 | padding: 32px !important; 323 | } 324 | 325 | .pt\+\+\+ { 326 | padding-top: 32px !important; 327 | } 328 | 329 | .pr\+\+\+ { 330 | padding-right: 32px !important; 331 | } 332 | 333 | .pb\+\+\+ { 334 | padding-bottom: 32px !important; 335 | } 336 | 337 | .pl\+\+\+ { 338 | padding-left: 32px !important; 339 | } 340 | 341 | .ph\+\+\+ { 342 | padding-right: 32px !important; 343 | padding-left: 32px !important; 344 | } 345 | 346 | .pv\+\+\+ { 347 | padding-top: 32px !important; 348 | padding-bottom: 32px !important; 349 | } 350 | 351 | .p0 { 352 | padding: 0 !important; 353 | } 354 | 355 | .pt0 { 356 | padding-top: 0 !important; 357 | } 358 | 359 | .pr0 { 360 | padding-right: 0 !important; 361 | } 362 | 363 | .pb0 { 364 | padding-bottom: 0 !important; 365 | } 366 | 367 | .pl0 { 368 | padding-left: 0 !important; 369 | } 370 | 371 | .ph0 { 372 | padding-right: 0 !important; 373 | padding-left: 0 !important; 374 | } 375 | 376 | .pv0 { 377 | padding-top: 0 !important; 378 | padding-bottom: 0 !important; 379 | } 380 | .h100 { 381 | height: 100% 382 | } 383 | 384 | .clearfix::after { 385 | clear: both; 386 | content: ""; 387 | display: table; 388 | } 389 | 390 | .float-right { 391 | float: right !important; 392 | } 393 | 394 | .float-left { 395 | float: left !important; 396 | } 397 | 398 | .float-none { 399 | float: none !important; 400 | } 401 | 402 | .text-left { 403 | text-align: left !important; 404 | } 405 | 406 | .text-center { 407 | text-align: center !important; 408 | } 409 | 410 | .text-right { 411 | text-align: right !important; 412 | } 413 | 414 | .display-block { 415 | display: block; 416 | } 417 | 418 | .dissolve-animation.ng-hide-remove, 419 | .dissolve-animation.ng-hide-add { 420 | -webkit-transition: 0.5s linear all; 421 | transition: 0.5s linear all; 422 | } 423 | 424 | .dissolve-animation.ng-hide-remove.ng-hide-remove-active, 425 | .dissolve-animation.ng-hide-add { 426 | opacity: 1; 427 | } 428 | 429 | .dissolve-animation.ng-hide-add.ng-hide-add-active, 430 | .dissolve-animation.ng-hide-remove { 431 | opacity: 0; 432 | } 433 | 434 | .icon-breath-animation { 435 | -webkit-animation: icon-breath 1.5s ease-in-out infinite; 436 | animation: icon-breath 1.5s ease-in-out infinite; 437 | } 438 | 439 | .icon-rotate-animation { 440 | -webkit-animation: icon-rotate 0.8s linear infinite; 441 | animation: icon-rotate 0.8s linear infinite; 442 | margin-right: 5px; 443 | } 444 | .no-txt-transform { 445 | text-transform: none; 446 | } 447 | 448 | @-webkit-keyframes icon-breath { 449 | from { 450 | box-shadow: 0 0 2px 5px rgba(255, 138, 138, 0.48); 451 | opacity: 0.5; 452 | } 453 | to { 454 | box-shadow: none; 455 | opacity: 1; 456 | } 457 | } 458 | 459 | @keyframes icon-breath { 460 | from { 461 | box-shadow: 0 0 2px 5px rgba(255, 138, 138, 0.48); 462 | opacity: 0.5; 463 | } 464 | to { 465 | box-shadow: none; 466 | opacity: 1; 467 | } 468 | } 469 | 470 | @-webkit-keyframes icon-rotate { 471 | from { 472 | -webkit-transform: rotate(0deg); 473 | transform: rotate(0deg); 474 | } 475 | to { 476 | -webkit-transform: rotate(360deg); 477 | transform: rotate(360deg); 478 | } 479 | } 480 | 481 | @keyframes icon-rotate { 482 | from { 483 | -webkit-transform: rotate(0deg); 484 | transform: rotate(0deg); 485 | } 486 | to { 487 | -webkit-transform: rotate(360deg); 488 | transform: rotate(360deg); 489 | } 490 | } 491 | 492 | @media only screen and (min-width: 0px) and (max-width: 600px) { 493 | .hide-sm { 494 | display: none; 495 | } 496 | } 497 | 498 | @media only screen and (min-width: 600px) { 499 | .hide-gt-sm { 500 | display: none; 501 | } 502 | } 503 | 504 | @media only screen and (min-width: 600px) and (max-width: 960px) { 505 | .hide-md { 506 | display: none; 507 | } 508 | } 509 | 510 | @media only screen and (min-width: 960px) { 511 | .hide-gt-md { 512 | display: none; 513 | } 514 | } 515 | 516 | @media only screen and (min-width: 960px) and (max-width: 1200px) { 517 | .hide-lg { 518 | display: none; 519 | } 520 | } 521 | 522 | @media only screen and (min-width: 1200px) { 523 | .hide-gt-lg { 524 | display: none; 525 | } 526 | } 527 | 528 | @media only screen and (min-width: 0px) and (max-width: 600px) { 529 | .lx-date-picker__day { 530 | line-height: 16px; 531 | } 532 | 533 | .lx-date-picker__day a { 534 | width: 16px; 535 | height: 16px; 536 | line-height: 16px; 537 | } 538 | } 539 | -------------------------------------------------------------------------------- /docs/styles/app-9c669350e7.css: -------------------------------------------------------------------------------- 1 | .mh,.mr{margin-right:8px!important}.mh,.ml{margin-left:8px!important}.mt,.mv{margin-top:8px!important}.mb,.mv{margin-bottom:8px!important}.ph,.pr{padding-right:8px!important}.ph,.pl{padding-left:8px!important}.pt,.pv{padding-top:8px!important}.pb,.pv{padding-bottom:8px!important}.login-view .login-checking,body .wrapper{-webkit-box-orient:vertical;-webkit-box-direction:normal}.login-view .card__header,.sidebar-view .sidebar .sidebar-menu .menu-item{border-bottom:1px solid rgba(0,0,0,.2)}.header-view .toolbar .fs-title a,a{text-decoration:none}.breadcrumb,.sidebar-view .sidebar .metismenu ul,.sidebar-view .sidebar .sidebar-menu{list-style:none}.width-100{width:100%}.width-50{width:50%}.pull-right{float:right}.pull-left{float:left}.m{margin:8px!important}.mh-,.mr-{margin-right:4px!important}.mh-,.ml-{margin-left:4px!important}.mt-,.mv-{margin-top:4px!important}.mb-,.mv-{margin-bottom:4px!important}.m-{margin:4px!important}.mh\+,.mr\+{margin-right:16px!important}.mh\+,.ml\+{margin-left:16px!important}.mt\+,.mv\+{margin-top:16px!important}.mb\+,.mv\+{margin-bottom:16px!important}.m\+{margin:16px!important}.mh\+\+,.mr\+\+{margin-right:24px!important}.mh\+\+,.ml\+\+{margin-left:24px!important}.mt\+\+,.mv\+\+{margin-top:24px!important}.mb\+\+,.mv\+\+{margin-bottom:24px!important}.m\+\+{margin:24px!important}.mh\+\+\+,.mr\+\+\+{margin-right:32px!important}.mh\+\+\+,.ml\+\+\+{margin-left:32px!important}.mt\+\+\+,.mv\+\+\+{margin-top:32px!important}.mb\+\+\+,.mv\+\+\+{margin-bottom:32px!important}.m\+\+\+{margin:32px!important}.mh0,.mr0{margin-right:0!important}.mh0,.ml0{margin-left:0!important}.mt0,.mv0{margin-top:0!important}.mb0,.mv0{margin-bottom:0!important}.m0{margin:0!important}.p{padding:8px!important}.ph-,.pr-{padding-right:4px!important}.ph-,.pl-{padding-left:4px!important}.pt-,.pv-{padding-top:4px!important}.pb-,.pv-{padding-bottom:4px!important}.p-{padding:4px!important}.ph\+,.pr\+{padding-right:16px!important}.ph\+,.pl\+{padding-left:16px!important}.pt\+,.pv\+{padding-top:16px!important}.pb\+,.pv\+{padding-bottom:16px!important}.p\+{padding:16px!important}.ph\+\+,.pr\+\+{padding-right:24px!important}.ph\+\+,.pl\+\+{padding-left:24px!important}.pt\+\+,.pv\+\+{padding-top:24px!important}.pb\+\+,.pv\+\+{padding-bottom:24px!important}.p\+\+{padding:24px!important}.ph\+\+\+,.pr\+\+\+{padding-right:32px!important}.ph\+\+\+,.pl\+\+\+{padding-left:32px!important}.pt\+\+\+,.pv\+\+\+{padding-top:32px!important}.pb\+\+\+,.pv\+\+\+{padding-bottom:32px!important}.p\+\+\+{padding:32px!important}.ph0,.pr0{padding-right:0!important}.ph0,.pl0{padding-left:0!important}.pt0,.pv0{padding-top:0!important}.pb0,.pv0{padding-bottom:0!important}.p0{padding:0!important}.h100{height:100%}.clearfix::after{clear:both;content:"";display:table}.float-right{float:right!important}.float-left{float:left!important}.float-none{float:none!important}.text-left{text-align:left!important}.text-center{text-align:center!important}.text-right{text-align:right!important}.display-block{display:block}.dissolve-animation.ng-hide-add,.dissolve-animation.ng-hide-remove{-webkit-transition:.5s linear all;transition:.5s linear all}.dissolve-animation.ng-hide-add,.dissolve-animation.ng-hide-remove.ng-hide-remove-active{opacity:1}.dissolve-animation.ng-hide-add.ng-hide-add-active,.dissolve-animation.ng-hide-remove{opacity:0}.icon-breath-animation{-webkit-animation:icon-breath 1.5s ease-in-out infinite;animation:icon-breath 1.5s ease-in-out infinite}.icon-rotate-animation{-webkit-animation:icon-rotate .8s linear infinite;animation:icon-rotate .8s linear infinite;margin-right:5px}.no-txt-transform{text-transform:none}@-webkit-keyframes icon-breath{from{box-shadow:0 0 2px 5px rgba(255,138,138,.48);opacity:.5}to{box-shadow:none;opacity:1}}@keyframes icon-breath{from{box-shadow:0 0 2px 5px rgba(255,138,138,.48);opacity:.5}to{box-shadow:none;opacity:1}}@-webkit-keyframes icon-rotate{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes icon-rotate{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@media only screen and (min-width:600px){.hide-gt-sm{display:none}}@media only screen and (min-width:600px) and (max-width:960px){.hide-md{display:none}}@media only screen and (min-width:960px){.hide-gt-md{display:none}}@media only screen and (min-width:960px) and (max-width:1200px){.hide-lg{display:none}}@media only screen and (min-width:1200px){.hide-gt-lg{display:none}}@media only screen and (min-width:0px) and (max-width:600px){.hide-sm{display:none}.lx-date-picker__day{line-height:16px}.lx-date-picker__day a{width:16px;height:16px;line-height:16px}}.breadcrumb{margin:0}.breadcrumb li{display:inline-block}body{font-family:Helvetica,arial}body .wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-height:100vh}.content,.content .main-content{display:-webkit-box;display:-ms-flexbox}.content{display:flex;height:100vh}.content.login{width:100%}.content.login .main-view{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.main-content.login .footer{position:fixed;bottom:0;left:0;right:0}@media only screen and (min-width:0px) and (max-width:600px){.content{margin-top:56px}.content .main-content .main-view{margin-left:8px;margin-right:8px}}.content .main-content{-webkit-box-flex:1;-ms-flex:1;flex:1;display:flex;-ms-flex-flow:column nowrap;flex-flow:column nowrap;overflow:auto;margin-top:40px;position:relative}.content .main-content .main-view{-webkit-box-flex:1;-ms-flex:1;flex:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center}md-sidenav[md-component-id=tableSiedenav]{width:50%!important;max-width:50%!important;height:100%;overflow:hidden}md-sidenav[md-component-id=tableSiedenav] .right-button{position:absolute;right:20px;top:20px;z-index:1}md-sidenav[md-component-id=tableSiedenav] .md-closed.md-locked-open-add-active,md-sidenav[md-component-id=tableSiedenav] .md-locked-open{width:50%!important;height:100%;max-width:50%!important}md-sidenav[md-component-id=tableSiedenav] md-content{position:absolute;top:0;bottom:0;width:100%;overflow:hidden;padding-top:0;padding-bottom:0}md-sidenav[md-component-id=tableSiedenav] md-content md-tab-content,md-sidenav[md-component-id=tableSiedenav] md-content md-tab-content>div{max-height:100%}.content .main-content .main-view.ng-enter{-webkit-animation:bounceIn .8s;animation:bounceIn .8s}.content .main-content .main-view .full-width{-ms-flex-item-align:start;align-self:flex-start;padding:10px 30px 20px;width:100%}.footer .copyright{margin:auto;text-shadow:1px 1px rgba(0,0,0,.2)}.footer .toolbar{background:#424242!important}.header-view{position:fixed;width:100%;z-index:2}body #loading-bar .bar{background:#2c3e50;height:3px}body #loading-bar .peg{box-shadow:0 0 10px #2c3e50,0 0 5px #2c3e50}body #loading-bar-spinner .spinner-icon{border-top-color:#2c3e50;border-left-color:#2c3e50}.sidebar-view .sidebar .sidebar-menu .menu-item.active{border-radius:5px;background-color:rgba(146,205,255,.42)}.sidebar-view .sidebar .sidebar-menu .menu-item.active i{color:#4caf50}.sidebar-view .sidebar .sidebar-menu .menu-item.active:hover{background-color:rgba(146,205,255,.42)}.sidebar-view .sidebar .sidebar-menu .menu-item:hover{text-shadow:1px 1px rgba(0,0,0,.2);background-color:#eee}.sidebar-view .sidebar .sidebar-menu .menu-item .link{display:block;text-decoration:none}.sidebar-view .sidebar .sidebar-menu .menu-item .link .text{line-height:32px;font-size:14pt}@media only screen and (min-width:0px) and (max-width:600px){.header-view .toolbar{padding-left:0;padding-right:0}.sidebar-view{position:fixed;height:100%;width:200px;box-shadow:3px 0 6px rgba(0,0,0,.2);-webkit-transform:translateX(-200px);transform:translateX(-200px);-webkit-transition:box-shadow,-webkit-transform .6s cubic-bezier(.23,1,.32,1);transition:box-shadow,-webkit-transform .6s cubic-bezier(.23,1,.32,1);transition:box-shadow,transform .6s cubic-bezier(.23,1,.32,1);transition:box-shadow,transform .6s cubic-bezier(.23,1,.32,1),-webkit-transform .6s cubic-bezier(.23,1,.32,1);z-index:999}.sidebar-view.show{-webkit-transform:none;transform:none}.sidebar-view .sidebar-backdrop{position:absolute;top:0;left:200px;z-index:990;background-color:rgba(0,0,0,.5);height:100%;width:100%}}.menu-button{width:180px;text-align:left;margin:0;padding:8px}.menu-button__title{font-size:16px;line-height:18px}.menu-button__subtitle{display:block;font-size:10px;line-height:12px;margin-bottom:8px}.header,.header md-card{height:40px;line-height:40px}.login-view{position:relative}.login-view .login-checking{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;z-index:10;background:rgba(0,0,0,.6);text-align:center;position:absolute;width:100%;height:100%}.login-view .login-checking .loading-text{-webkit-animation:pulse .8s linear infinite;animation:pulse .8s linear infinite}.login-view .login-message{padding:8px}.login-view .login-message.error{background-color:#f44336}.login-view .login-message.success{background-color:#4caf50}md-input-container{margin:4px 0}a{color:inherit}md-toast.md-dark-theme .md-toast-content{background-color:#666}md-card{border-radius:0}.sidebar-view{border-right:1px solid rgba(0,0,0,.2);margin-right:14px;margin-top:40px}.sidebar-view.login{z-index:3}.sidebar-view .sidebar{overflow:hidden;height:100%;padding-left:5px}.sidebar-view .sidebar .mCSB_container_wrapper{margin-right:5px!important}.sidebar-view .sidebar .mCSB_container_wrapper .mCSB_container{padding-right:0!important}.sidebar-view .sidebar .metismenu{-webkit-margin-before:0;-webkit-padding-start:0}.sidebar-view .sidebar .metismenu ul li{position:relative;padding-right:5px}.sidebar-view .sidebar .metismenu ul li ul.sub-menu{-webkit-padding-start:0;margin-left:20px}.sidebar-view .sidebar .metismenu ul li button{width:100%;text-overflow:ellipsis;margin:0}.sidebar-view .sidebar .metismenu ul li button i{margin-right:5px}.logo{width:24px;height:24px;margin-left:10px;float:left;padding-top:7px}.sql-table th{background-color:rgba(200,200,200,.2)}.sql-table td,.sql-table th{white-space:nowrap;text-align:left;padding:5px}.sql-table .sql-table__subheader{font-size:80%;opacity:.7}pre{-moz-tab-size:2;-o-tab-size:2;tab-size:2}.fullscreen{width:100%;height:100%}.fullscreen.default{background-color:#fafafa}.fullscreen.dark{background-color:#303030}:-webkit-full-screen{background-color:inherit}.sql-tabs md-tabs-wrapper{padding-right:48px}.sql-tabs md-next-button{right:48px} -------------------------------------------------------------------------------- /docs/styles/app-7900bfb0cd.css: -------------------------------------------------------------------------------- 1 | .mh,.mr{margin-right:8px!important}.mh,.ml{margin-left:8px!important}.mt,.mv{margin-top:8px!important}.mb,.mv{margin-bottom:8px!important}.ph,.pr{padding-right:8px!important}.ph,.pl{padding-left:8px!important}.pt,.pv{padding-top:8px!important}.pb,.pv{padding-bottom:8px!important}.content .main-content,.login-view .login-checking,body .wrapper{-webkit-box-orient:vertical;-webkit-box-direction:normal}.login-view .card__header,.sidebar-view .sidebar .sidebar-menu .menu-item{border-bottom:1px solid rgba(0,0,0,.2)}.header-view .toolbar .fs-title a,a{text-decoration:none}.breadcrumb,.sidebar-view .sidebar .metismenu ul,.sidebar-view .sidebar .sidebar-menu{list-style:none}.width-100{width:100%}.width-50{width:50%}.pull-right{float:right}.pull-left{float:left}.m{margin:8px!important}.mh-,.mr-{margin-right:4px!important}.mh-,.ml-{margin-left:4px!important}.mt-,.mv-{margin-top:4px!important}.mb-,.mv-{margin-bottom:4px!important}.m-{margin:4px!important}.mh\+,.mr\+{margin-right:16px!important}.mh\+,.ml\+{margin-left:16px!important}.mt\+,.mv\+{margin-top:16px!important}.mb\+,.mv\+{margin-bottom:16px!important}.m\+{margin:16px!important}.mh\+\+,.mr\+\+{margin-right:24px!important}.mh\+\+,.ml\+\+{margin-left:24px!important}.mt\+\+,.mv\+\+{margin-top:24px!important}.mb\+\+,.mv\+\+{margin-bottom:24px!important}.m\+\+{margin:24px!important}.mh\+\+\+,.mr\+\+\+{margin-right:32px!important}.mh\+\+\+,.ml\+\+\+{margin-left:32px!important}.mt\+\+\+,.mv\+\+\+{margin-top:32px!important}.mb\+\+\+,.mv\+\+\+{margin-bottom:32px!important}.m\+\+\+{margin:32px!important}.mh0,.mr0{margin-right:0!important}.mh0,.ml0{margin-left:0!important}.mt0,.mv0{margin-top:0!important}.mb0,.mv0{margin-bottom:0!important}.m0{margin:0!important}.p{padding:8px!important}.ph-,.pr-{padding-right:4px!important}.ph-,.pl-{padding-left:4px!important}.pt-,.pv-{padding-top:4px!important}.pb-,.pv-{padding-bottom:4px!important}.p-{padding:4px!important}.ph\+,.pr\+{padding-right:16px!important}.ph\+,.pl\+{padding-left:16px!important}.pt\+,.pv\+{padding-top:16px!important}.pb\+,.pv\+{padding-bottom:16px!important}.p\+{padding:16px!important}.ph\+\+,.pr\+\+{padding-right:24px!important}.ph\+\+,.pl\+\+{padding-left:24px!important}.pt\+\+,.pv\+\+{padding-top:24px!important}.pb\+\+,.pv\+\+{padding-bottom:24px!important}.p\+\+{padding:24px!important}.ph\+\+\+,.pr\+\+\+{padding-right:32px!important}.ph\+\+\+,.pl\+\+\+{padding-left:32px!important}.pt\+\+\+,.pv\+\+\+{padding-top:32px!important}.pb\+\+\+,.pv\+\+\+{padding-bottom:32px!important}.p\+\+\+{padding:32px!important}.ph0,.pr0{padding-right:0!important}.ph0,.pl0{padding-left:0!important}.pt0,.pv0{padding-top:0!important}.pb0,.pv0{padding-bottom:0!important}.p0{padding:0!important}.h100{height:100%}.clearfix::after{clear:both;content:"";display:table}.float-right{float:right!important}.float-left{float:left!important}.float-none{float:none!important}.text-left{text-align:left!important}.text-center{text-align:center!important}.text-right{text-align:right!important}.display-block{display:block}.dissolve-animation.ng-hide-add,.dissolve-animation.ng-hide-remove{-webkit-transition:.5s linear all;transition:.5s linear all}.dissolve-animation.ng-hide-add,.dissolve-animation.ng-hide-remove.ng-hide-remove-active{opacity:1}.dissolve-animation.ng-hide-add.ng-hide-add-active,.dissolve-animation.ng-hide-remove{opacity:0}.icon-breath-animation{-webkit-animation:icon-breath 1.5s ease-in-out infinite;animation:icon-breath 1.5s ease-in-out infinite}.icon-rotate-animation{-webkit-animation:icon-rotate .8s linear infinite;animation:icon-rotate .8s linear infinite;margin-right:5px}.no-txt-transform{text-transform:none}@-webkit-keyframes icon-breath{from{box-shadow:0 0 2px 5px rgba(255,138,138,.48);opacity:.5}to{box-shadow:none;opacity:1}}@keyframes icon-breath{from{box-shadow:0 0 2px 5px rgba(255,138,138,.48);opacity:.5}to{box-shadow:none;opacity:1}}@-webkit-keyframes icon-rotate{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@keyframes icon-rotate{from{-webkit-transform:rotate(0);transform:rotate(0)}to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@media only screen and (min-width:600px){.hide-gt-sm{display:none}}@media only screen and (min-width:600px) and (max-width:960px){.hide-md{display:none}}@media only screen and (min-width:960px){.hide-gt-md{display:none}}@media only screen and (min-width:960px) and (max-width:1200px){.hide-lg{display:none}}@media only screen and (min-width:1200px){.hide-gt-lg{display:none}}@media only screen and (min-width:0px) and (max-width:600px){.hide-sm{display:none}.lx-date-picker__day{line-height:16px}.lx-date-picker__day a{width:16px;height:16px;line-height:16px}}.breadcrumb{margin:0}.breadcrumb li{display:inline-block}body{font-family:Helvetica,arial}body .wrapper{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;min-height:100vh}.content,.content .main-content{display:-webkit-box;display:-ms-flexbox}.content{display:flex;height:100vh}.content.login{width:100%}.content.login .main-view{-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center}.main-content.login .footer{position:fixed;bottom:0;left:0;right:0}@media only screen and (min-width:0px) and (max-width:600px){.content{margin-top:56px}.content .main-content .main-view{margin-left:8px;margin-right:8px}}.content .main-content{-webkit-box-flex:1;-ms-flex:1;flex:1;display:flex;-ms-flex-flow:column nowrap;flex-flow:column nowrap;overflow:auto;margin-top:40px;position:relative}.content .main-content .main-view{-webkit-box-flex:1;-ms-flex:1;flex:1;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center}md-sidenav[md-component-id=tableSiedenav]{width:50%!important;max-width:50%!important;height:100%;overflow:hidden}md-sidenav[md-component-id=tableSiedenav] .right-button{position:absolute;right:20px;top:20px;z-index:1}md-sidenav[md-component-id=tableSiedenav] .md-closed.md-locked-open-add-active,md-sidenav[md-component-id=tableSiedenav] .md-locked-open{width:50%!important;height:100%;max-width:50%!important}md-sidenav[md-component-id=tableSiedenav] md-content{position:absolute;top:0;bottom:0;width:100%;overflow:hidden;padding-top:0;padding-bottom:0}md-sidenav[md-component-id=tableSiedenav] md-content md-tab-content,md-sidenav[md-component-id=tableSiedenav] md-content md-tab-content>div{max-height:100%}.content .main-content .main-view.ng-enter{-webkit-animation:bounceIn .8s;animation:bounceIn .8s}.content .main-content .main-view .full-width{-ms-flex-item-align:start;align-self:flex-start;padding:10px 30px 20px;width:100%}.footer .copyright{margin:auto;text-shadow:1px 1px rgba(0,0,0,.2)}.footer .toolbar{background:#424242!important}.header-view{position:fixed;width:100%;z-index:2}body #loading-bar .bar{background:#2c3e50;height:3px}body #loading-bar .peg{box-shadow:0 0 10px #2c3e50,0 0 5px #2c3e50}body #loading-bar-spinner .spinner-icon{border-top-color:#2c3e50;border-left-color:#2c3e50}.sidebar-view .sidebar .sidebar-menu .menu-item.active{border-radius:5px;background-color:rgba(146,205,255,.42)}.sidebar-view .sidebar .sidebar-menu .menu-item.active i{color:#4caf50}.sidebar-view .sidebar .sidebar-menu .menu-item.active:hover{background-color:rgba(146,205,255,.42)}.sidebar-view .sidebar .sidebar-menu .menu-item:hover{text-shadow:1px 1px rgba(0,0,0,.2);background-color:#eee}.sidebar-view .sidebar .sidebar-menu .menu-item .link{display:block;text-decoration:none}.sidebar-view .sidebar .sidebar-menu .menu-item .link .text{line-height:32px;font-size:14pt}@media only screen and (min-width:0px) and (max-width:600px){.header-view .toolbar{padding-left:0;padding-right:0}.sidebar-view{position:fixed;height:100%;width:200px;box-shadow:3px 0 6px rgba(0,0,0,.2);-webkit-transform:translateX(-200px);transform:translateX(-200px);-webkit-transition:box-shadow,-webkit-transform .6s cubic-bezier(.23,1,.32,1);transition:box-shadow,-webkit-transform .6s cubic-bezier(.23,1,.32,1);transition:box-shadow,transform .6s cubic-bezier(.23,1,.32,1);transition:box-shadow,transform .6s cubic-bezier(.23,1,.32,1),-webkit-transform .6s cubic-bezier(.23,1,.32,1);z-index:999}.sidebar-view.show{-webkit-transform:none;transform:none}.sidebar-view .sidebar-backdrop{position:absolute;top:0;left:200px;z-index:990;background-color:rgba(0,0,0,.5);height:100%;width:100%}}.menu-button{width:180px;text-align:left;margin:0;padding:8px}.menu-button__title{font-size:16px;line-height:18px}.menu-button__subtitle{display:block;font-size:10px;line-height:12px;margin-bottom:8px}.header,.header md-card{height:40px;line-height:40px}.login-view{position:relative}.login-view .login-checking{display:-webkit-box;display:-ms-flexbox;display:flex;-ms-flex-direction:column;flex-direction:column;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;z-index:10;background:rgba(0,0,0,.6);text-align:center;position:absolute;width:100%;height:100%}.login-view .login-checking .loading-text{-webkit-animation:pulse .8s linear infinite;animation:pulse .8s linear infinite}.login-view .login-message{padding:8px}.login-view .login-message.error{background-color:#f44336}.login-view .login-message.success{background-color:#4caf50}md-input-container{margin:4px 0}a{color:inherit}md-toast.md-dark-theme .md-toast-content{background-color:#666}md-card{border-radius:0}.sidebar-view{border-right:1px solid rgba(0,0,0,.2);margin-right:14px;margin-top:40px}.sidebar-view.login{z-index:3}.sidebar-view .sidebar{overflow:hidden;height:100%;padding-left:5px}.sidebar-view .sidebar .mCSB_container_wrapper{margin-right:5px!important}.sidebar-view .sidebar .mCSB_container_wrapper .mCSB_container{padding-right:0!important}.sidebar-view .sidebar .metismenu{-webkit-margin-before:0;-webkit-padding-start:0}.sidebar-view .sidebar .metismenu ul li{position:relative;padding-right:5px}.sidebar-view .sidebar .metismenu ul li ul.sub-menu{-webkit-padding-start:0;margin-left:20px}.sidebar-view .sidebar .metismenu ul li button{width:100%;text-overflow:ellipsis;margin:0}.sidebar-view .sidebar .metismenu ul li button i{margin-right:5px}.logo{width:24px;height:24px;margin-left:10px;float:left;padding-top:7px}.sql-table th{background-color:rgba(200,200,200,.2)}.sql-table td,.sql-table th{white-space:nowrap;text-align:left;padding:5px}.sql-table .sql-table__subheader{font-size:80%;opacity:.7}pre{-moz-tab-size:2;-o-tab-size:2;tab-size:2}.fullscreen{width:100%;height:100%}.fullscreen.default{background-color:#fafafa}.fullscreen.dark{background-color:#303030}:-webkit-full-screen{background-color:inherit}.sql-tabs md-tabs-wrapper{padding-right:48px}.sql-tabs md-next-button{right:48px} -------------------------------------------------------------------------------- /src/app/sql/ace-mode-clickhouse.js: -------------------------------------------------------------------------------- 1 | var define = window.define || window.ace.define; 2 | 3 | define("ace/mode/clickhouse_highlight_rules", ["$rootScope","require", "exports", "module", "ace/lib/oop", "ace/mode/text_highlight_rules"], function(require, exports) { 4 | "use strict"; 5 | 6 | var oop = require("../lib/oop"); 7 | var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules; 8 | 9 | 10 | var ClickhouseHighlightRules = function() { 11 | var keywords = ( 12 | "SELECT|INSERT|UPDATE|DELETE|FROM|WHERE|AND|OR|LIMIT|OFFSET|HAVING|AS|" + 13 | "WHEN|ELSE|END|TYPE|LEFT|RIGHT|JOIN|ON|OUTER|DESC|ASC|UNION|CREATE|TABLE|PRIMARY|KEY|" + 14 | "FOREIGN|NOT|REFERENCES|DEFAULT|NULL|INNER|CROSS|NATURAL|DATABASE|DROP|GRANT|" + 15 | "ANY|ATTACH|DETACH|DESCRIBE|OPTIMIZE|PREWHERE|TOTALS|DATABASES|PROCESSLIST|SHOW|IF" 16 | ); 17 | var identifier = "[$A-Za-z_\\x7f-\\uffff][$\\w\\x7f-\\uffff]*"; 18 | var keywordsDouble="IF\\W+NOT\\W+EXISTS|IF\\W+EXISTS|FORMAT\\W+Vertical|FORMAT\\W+JSONCompact|FORMAT\\W+JSONEachRow|FORMAT\\W+TSKV|FORMAT\\W+TabSeparatedWithNames|FORMAT\\W+TabSeparatedWithNamesAndTypes|FORMAT\\W+TabSeparatedRaw|FORMAT\\W+BlockTabSeparated|FORMAT\\W+CSVWithNames|FORMAT\\W+CSV|FORMAT\\W+JSON|FORMAT\\W+TabSeparated"; 19 | 20 | var builtinConstants = ( 21 | "true|false" 22 | ); 23 | 24 | var builtinFunctions = ( 25 | "avg|count|first|last|max|min|sum|ucase|lcase|mid|len|round|rank|now|" + 26 | "coalesce|ifnull|isnull|nvl|countIf|timeSlot|yesterday|today|now|toRelativeSecondNum|" + "toRelativeMinuteNum|toRelativeHourNum|toRelativeDayNum|toRelativeWeekNum|toRelativeMonthNum|" + 27 | "toRelativeYearNum|toTime|toStartOfHour|toStartOfFiveMinute|toStartOfMinute|toStartOfYear|" + 28 | "toStartOfQuarter|toStartOfMonth|toMonday|toSecond|toMinute|toHour|toDayOfWeek|toDayOfMonth|" + 29 | "toMonth|toYear|toFixedString|toStringCutToZero|reinterpretAsString|reinterpretAsDate|" + 30 | "reinterpretAsDateTime|reinterpretAsFloat32|reinterpretAsFloat64|reinterpretAsInt8|" + 31 | "reinterpretAsInt16|reinterpretAsInt32|reinterpretAsInt64|reinterpretAsUInt8|" + 32 | "reinterpretAsUInt16|reinterpretAsUInt32|reinterpretAsUInt64|toUInt8|toUInt16|toUInt32|" + 33 | "toUInt64|toInt8|toInt16|toInt32|toInt64|toFloat32|toFloat64|toDate|toDateTime|toString|" + 34 | "bitAnd|bitOr|bitXor|bitNot|bitShiftLeft|bitShiftRight|abs|negate|modulo|intDivOrZero|" + 35 | "intDiv|divide|multiply|minus|plus|empty|notEmpty|length|lengthUTF8|lower|upper|lowerUTF8|" + 36 | "upperUTF8|reverse|reverseUTF8|concat|substring|substringUTF8|appendTrailingCharIfAbsent|" + 37 | "position|positionUTF8|match|extract|extractAll|like|notLike|replaceOne|replaceAll|" + 38 | "replaceRegexpOne|range|arrayElement|has|indexOf|countEqual|arrayEnumerate|arrayEnumerateUniq|" + 39 | "arrayJoin|arrayMap|arrayFilter|arrayExists|arrayCount|arrayAll|arrayFirst|arraySum|splitByChar|" + 40 | "splitByString|alphaTokens|domainWithoutWWW|topLevelDomain|firstSignificantSubdomain|" + 41 | "cutToFirstSignificantSubdomain|queryString|URLPathHierarchy|URLHierarchy|extractURLParameterNames|" + 42 | "extractURLParameters|extractURLParameter|queryStringAndFragment|cutWWW|cutQueryString|" + 43 | "cutFragment|cutQueryStringAndFragment|cutURLParameter|IPv4NumToString|IPv4StringToNum|" + 44 | "IPv4NumToStringClassC|IPv6NumToString|IPv6StringToNum|rand|rand64|halfMD5|MD5|sipHash64|" + 45 | "sipHash128|cityHash64|intHash32|intHash64|SHA1|SHA224|SHA256|URLHash|hex|unhex|bitmaskToList|" + 46 | "bitmaskToArray|floor|ceil|round|roundToExp2|roundDuration|roundAge|regionToCountry|" + 47 | "regionToContinent|regionToPopulation|regionIn|regionHierarchy|regionToName|OSToRoot|OSIn|" + 48 | "OSHierarchy|SEToRoot|SEIn|SEHierarchy|dictGetUInt8|dictGetUInt16|dictGetUInt32|" + 49 | "dictGetUInt64|dictGetInt8|dictGetInt16|dictGetInt32|dictGetInt64|dictGetFloat32|" + 50 | "dictGetFloat64|dictGetDate|dictGetDateTime|dictGetString|dictGetHierarchy|dictHas|dictIsIn|" + 51 | "uniq|argMin|argMax|uniqCombined|uniqHLL12|uniqExact|groupArray|groupUniqArray|quantile|" + 52 | "quantileDeterministic|quantileTiming|quantileTimingWeighted|quantileExact|" + 53 | "quantileExactWeighted|quantileTDigest|median|quantiles|varSamp|varPop|stddevSamp|stddevPop|" + 54 | "covarSamp|covarPop|corr|sequenceMatch|sequenceCount|uniqUpTo|countIf|avgIf|" + 55 | "quantilesTimingIf|argMinIf|uniqArray|sumArray|quantilesTimingArrayIf|uniqArrayIf|medianIf|" + 56 | "quantilesIf|varSampIf|varPopIf|stddevSampIf|stddevPopIf|covarSampIf|covarPopIf|corrIf|" + 57 | "uniqArrayIf|sumArrayIf" 58 | ); 59 | 60 | var dataTypes = ( 61 | "int|numeric|decimal|date|varchar|char|bigint|float|double|bit|binary|text|set|timestamp|" + 62 | "money|real|number|integer|" + 63 | "uint8|uint16|uint32|uint64|int8|int16|int32|int64|float32|float64|datetime|enum8|enum16|" + 64 | "array|tuple|string" 65 | ); 66 | 67 | var keywordMapper = this.createKeywordMapper({ 68 | "support.function": builtinFunctions, 69 | "keyword": keywords, 70 | "constant.language": builtinConstants, 71 | "storage.type": dataTypes, 72 | "markup.bold":window.global_keywords_tables, 73 | "markup.heading":window.global_keywords_fields 74 | }, "identifier", true); 75 | 76 | 77 | this.$rules = { 78 | "start": [{ 79 | token: "comment", 80 | regex: "--.*$", 81 | caseInsensitive: true 82 | },{ 83 | token: "keyword", 84 | regex: "GROUP\\W+BY|ORDER\\W+BY" 85 | }, 86 | { 87 | token: "comment", 88 | start: "/\\*", 89 | end: "\\*/" 90 | }, { 91 | token: "string", // " string 92 | regex: '".*?"' 93 | },{ 94 | token: "storage", 95 | regex: keywordsDouble 96 | }, { 97 | token: "string", // ' string 98 | regex: "'.*?'" 99 | }, { 100 | token: "constant.numeric", // float 101 | regex: "[+-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b" 102 | }, { 103 | token: keywordMapper, 104 | regex: "[a-zA-Z_$][a-zA-Z0-9_$]*\\b" 105 | }, 106 | { 107 | token: "constant.character.escape", 108 | regex: /;{2}/ 109 | }, 110 | { 111 | token: "punctuation", 112 | regex: /[?:,;.]/, 113 | }, 114 | { 115 | token: "keyword.operator", 116 | regex: "\\+|\\-|\\/|\\/\\/|%|<@>|@>|<@|&|\\^|~|<|>|<=|=>|==|!=|<>|=" 117 | }, { 118 | token: "paren.lparen", 119 | regex: "[\\(]" 120 | }, { 121 | token: "paren.rparen", 122 | regex: "[\\)]" 123 | }, { 124 | token: "text", 125 | regex: "\\s+" 126 | } 127 | 128 | ] 129 | }; 130 | this.normalizeRules(); 131 | 132 | var completions = []; 133 | var addCompletions = function(arr, meta) { 134 | arr.forEach(function(v) { 135 | completions.push({ 136 | name: v, 137 | value: v, 138 | score: 0, 139 | meta: meta 140 | }); 141 | }); 142 | }; 143 | 144 | addCompletions(builtinFunctions.split('|'), 'function'); 145 | addCompletions(keywords.split('|'), 'keyword'); 146 | addCompletions("GROUP BY|ORDER BY|FORMAT JSON|FORMAT JSONCompact|FORMAT JSONEachRow|FORMAT TSKV|FORMAT TabSeparated|FORMAT TabSeparatedWithNames|FORMAT TabSeparatedWithNamesAndTypes|FORMAT TabSeparatedRaw|FORMAT BlockTabSeparated|FORMAT CSV|FORMAT CSVWithNames".split('|'), 'keyword'); 147 | addCompletions(dataTypes.split('|'), 'type'); 148 | addCompletions(window.global_keywords_tables.split('|'), 'storage'); 149 | addCompletions(window.global_keywords_fields.split('|'), 'storage'); 150 | //this allows for custom 'meta' and proper case of completions 151 | this.completions = completions; 152 | 153 | }; 154 | 155 | oop.inherits(ClickhouseHighlightRules, TextHighlightRules); 156 | exports.ClickhouseHighlightRules = ClickhouseHighlightRules; 157 | }); 158 | 159 | define("ace/mode/clickhouse", ["require", "exports", "module", "ace/lib/oop", "ace/mode/text","ace/token_iterator", "ace/mode/clickhouse_highlight_rules", "ace/range"], function(require, exports) { 160 | "use strict"; 161 | 162 | var oop = require("../lib/oop"); 163 | var TextMode = require("./text").Mode; 164 | var ClickhouseHighlightRules = require("./clickhouse_highlight_rules").ClickhouseHighlightRules; 165 | 166 | var Mode = function() { 167 | 168 | this.HighlightRules = ClickhouseHighlightRules; 169 | }; 170 | oop.inherits(Mode, TextMode); 171 | 172 | (function() { 173 | 174 | this.lineCommentStart = "--"; 175 | this.getCompletions = function(state, session) { 176 | return session.$mode.$highlightRules.completions; 177 | }; 178 | 179 | this.$id = "ace/mode/clickhouse"; 180 | // --------------------------------------------------------------------------- 181 | this.findTokens = function(sql,type,needfirst) { 182 | sql=sql.replace(/^(\r\n|\n|\r)/gm,"").replace(/(\r\n|\n|\r)$/gm,""); 183 | var TokenIterator = require("ace/token_iterator").TokenIterator; 184 | var EditSession = require("ace/edit_session").EditSession; 185 | 186 | var session = new EditSession(sql,this); 187 | var iterator = new TokenIterator(session, 0, 0); 188 | var token = iterator.getCurrentToken(); 189 | var matches=[]; 190 | var startRow= 0, startCol=0; 191 | 192 | while (token) { 193 | var t = token; 194 | t['row'] = iterator.getCurrentTokenRow(); 195 | t['col'] = iterator.getCurrentTokenColumn(); 196 | if (t.type == type ) { 197 | matches.push(t); 198 | if (needfirst) 199 | { 200 | t.value=t.value.toLowerCase(); 201 | return t; 202 | } 203 | } 204 | token = iterator.stepForward(); 205 | }//w 206 | return matches; 207 | }; 208 | this.splitByTokens = function(sql,type,value) { 209 | sql=sql.replace(/^(\r\n|\n|\r)/gm,"").replace(/(\r\n|\n|\r)$/gm,""); 210 | 211 | var TokenIterator = require("ace/token_iterator").TokenIterator; 212 | var EditSession = require("ace/edit_session").EditSession; 213 | var Range = require("ace/range").Range; 214 | 215 | var session = new EditSession(sql,this); 216 | var iterator = new TokenIterator(session, 0, 0); 217 | var token = iterator.getCurrentToken(); 218 | var matches=[]; 219 | var startRow= 0, startCol=0; 220 | 221 | while (token) { 222 | var t=token; 223 | t['row']=iterator.getCurrentTokenRow(); 224 | t['col']=iterator.getCurrentTokenColumn(); 225 | if ( t.type == type && t.value == value) 226 | { 227 | //var session = new EditSession(sql,this); 228 | //session.clearSelection(); 229 | var range1 = new Range(startRow, startCol, t.row, t.col+value.length); 230 | var text=session.getTextRange(range1); 231 | startRow= t.row; 232 | startCol= t.col+value.length; 233 | text=text.trim().replace(new RegExp("^"+value+"|"+value+'$','g'),"").trim().replace(/^(\r\n|\n|\r)/gm,"").replace(/(\r\n|\n|\r)$/gm,""); 234 | if (text.length>2) 235 | { 236 | matches.push({sql:text,range:range1}); 237 | } 238 | } 239 | token = iterator.stepForward(); 240 | } 241 | var range1 = new Range(startRow, startCol,Number.MAX_VALUE,Number.MAX_VALUE); 242 | var text=session.getTextRange(range1); 243 | text=text.trim().replace("^("+value+")","").replace(value+"$","").trim().replace(/^(\r\n|\n|\r)/gm,"").replace(/(\r\n|\n|\r)$/gm,""); 244 | text=text.replace(new RegExp("^"+value+"|"+value+'$','g'),"").trim().replace(/^(\r\n|\n|\r)/gm,"").replace(/(\r\n|\n|\r)$/gm,""); 245 | if (text.length>2) 246 | { 247 | 248 | matches.push({sql:text,range:range1}); 249 | } 250 | return matches; 251 | }; 252 | 253 | }).call(Mode.prototype); 254 | 255 | exports.Mode = Mode; 256 | 257 | }); 258 | -------------------------------------------------------------------------------- /src/app/sql/sql.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | {{'Настройки редактора' | translate}} 5 | 6 |
7 | 8 |
9 | 10 | 11 | 12 | {{tab.name + (tab.changed ? ' *' : '')}} 13 | 14 | 15 | 16 | 17 |
18 |
19 |
20 |
29 |
30 |
31 | {{tab.buttonTitle}} 32 |
33 | 34 |
35 |
36 |
37 | 38 | 39 | {{'На весь экран' | translate}} 40 | 41 | 42 | 43 | {{'Сохранить' | translate}} 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | {{item.name}} 53 | {{'История зарпросов' | translate}} 54 |
55 | 56 |
57 |
58 |
59 |
60 |
61 | 62 | 63 | 64 | {{'Словари' | translate}} 65 | 66 | 67 | 68 | 69 | {{dictionary.title}} 70 | 71 | 72 | 73 | 74 | 75 | 76 | {{'Лог запросов' | translate}} 77 | 78 |
79 |
80 | 81 | 82 | 83 | 84 | {{::result.time}} 85 | 86 | 87 | 88 | 89 |
90 | 91 |
92 |
93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 108 | {{'CSV с заголовком' | translate}} 109 | 110 | 111 | 112 | 117 | {{'CSV без заголовка' | translate}} 118 | 119 | 120 | 121 | 128 | {{'Табулированный текст с заголовками' | translate}} 129 | 130 | 131 | 132 | 139 | {{'Табулированный текст без заголовков' | translate}} 140 | 141 | 142 | 143 | 144 |
145 | 146 |
147 | {{'время' | translate}}: {{item.statistics.elapsed | number:3}}, 148 | {{'строк прочитано' | translate}}: {{item.statistics.rows_read | number}}, 149 | {{'прочитано' | translate}}: {{item.statistics.bytes_read | filesize}} 150 |
151 |
152 |
153 | 154 |
155 | {{'нет данных' | translate}} 156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 | 165 | 166 | 167 | 168 | 169 |
170 |
171 | 172 | 173 |
174 |

175 | {{'Настройки редактора' | translate}} 176 |

177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | {{format.name}} 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | {{theme}} 198 | 199 | 200 | 201 | 202 | 203 | 204 | {{theme.title}} 205 | 206 | 207 | 208 | 209 | {{'Сохранять сессию' | translate}} 210 | 211 | 212 | 213 | 214 | {{'Закрыть' | translate}} 215 | 216 |
217 |
218 | 219 | 220 |
221 |

222 | 223 | 224 | {{'Сохранить' | translate}} 225 | 226 | {{'Лог запросов' | translate}} 227 |

228 |
229 |
230 |
{{::sql}}
231 |
232 | 233 | {{'История запросов пуста' | translate}} 234 | 235 |
236 | -------------------------------------------------------------------------------- /src/app/sql/sql.js: -------------------------------------------------------------------------------- 1 | // @todo : костылик , не получилось сделать http://stackoverflow.com/questions/22166784/dynamically-update-syntax-highlighting-mode-rules-for-the-ace-editor 2 | window.global_keywords_fields = ""; 3 | window.global_keywords_tables = ""; 4 | 5 | ((angular, smi2) => { 6 | 'use strict'; 7 | 8 | angular.module(smi2.app.name).controller('SqlController', SqlController); 9 | SqlController.$inject = [ 10 | '$scope', 11 | '$rootScope', 12 | '$window', 13 | 'localStorageService', 14 | 'API', 15 | '$mdSidenav', 16 | '$mdDialog', 17 | '$mdToast', 18 | 'ThemeService', 19 | '$timeout', 20 | '$filter' 21 | ]; 22 | 23 | /** 24 | * @ngdoc controller 25 | * @name smi2.controller:SqlController 26 | * @description SQL controller data 27 | */ 28 | function SqlController($scope, 29 | $rootScope, 30 | $window, 31 | localStorageService, 32 | API, 33 | $mdSidenav, 34 | $mdDialog, 35 | $mdToast, 36 | ThemeService, 37 | $timeout, 38 | $filter) { 39 | 40 | const SQL_HISTORY_KEY = 'sqlHistory2'; 41 | const SQL_LOG_KEY = 'sqlLog'; 42 | const SQL_SAVE_TABS_KEY = 'saveTabs'; 43 | const SQL_SESSION_KEY = 'sessionData'; 44 | const SQL_LOG_LENGTH = 30; 45 | 46 | $scope.vars = { 47 | sqlHistory: localStorageService.get(SQL_HISTORY_KEY) || [], 48 | dictionaries: [], 49 | tabs: [], 50 | saveTabs: localStorageService.get(SQL_SAVE_TABS_KEY) || false, 51 | uiTheme: ThemeService.themeObject, 52 | uiThemes: ThemeService.list, 53 | currentTab: {}, 54 | selectedTab: 0, 55 | sqlLog: localStorageService.get(SQL_LOG_KEY) || [], 56 | formats: [{ 57 | name: $filter('translate')('Таблица'), 58 | sql: ' format JSON', 59 | render: 'html' 60 | }, { 61 | name: 'JSON compact', 62 | sql: ' format JSONCompact' 63 | } 64 | ], 65 | db: null, 66 | limitRows: localStorageService.get('editorLimitRows') || 500, 67 | fontSize: localStorageService.get('editorFontSize') || 16, 68 | theme: localStorageService.get('editorTheme') || 'cobalt' 69 | }; 70 | $scope.vars.format = $scope.vars.formats[0]; 71 | $scope.vars.themes = [ 72 | 'ambiance', 73 | 'eclipse', 'mono_industrial', 'tomorrow_night_blue', 74 | 'chaos', 'github', 'monokai', 'tomorrow_night_bright', 75 | 'chrome', 'idle_fingers', 'pastel_on_dark', 'tomorrow_night_eighties', 76 | 'clouds', 'iplastic', 'solarized_dark', 'tomorrow_night', 77 | 'clouds_midnight', 'katzenmilch', 'solarized_light', 'twilight', 78 | 'cobalt', 'kr_theme', 'sqlserver', 'vibrant_ink', 79 | 'crimson_editor', 'kuroir', 'terminal', 'xcode', 80 | 'dawn', 'merbivore', 'textmate', 81 | 'dreamweaver', 'merbivore_soft', 'tomorrow' 82 | ]; 83 | $rootScope.breadcrumbs = [{ 84 | text: 'SQL', 85 | link: 'sql' 86 | }]; 87 | 88 | /** 89 | * Prevent data loss on close page 90 | */ 91 | $window.onbeforeunload = (event) => { 92 | if ($scope.vars.currentTab.sql !== '' && location.hostname != 'localhost') { 93 | let message = $filter('translate')('Хотите покинуть страницу?'); 94 | if (typeof event == 'undefined') { 95 | event = window.event; 96 | } 97 | if (event) { 98 | event.returnValue = message; 99 | } 100 | return message; 101 | } 102 | }; 103 | 104 | /** 105 | * Prevent data loss on state change 106 | */ 107 | const clearRouterListener = $scope.$on('$stateChangeStart', (event) => { 108 | let message = $filter('translate')('Хотите покинуть страницу?'); 109 | if (!event.defaultPrevented && $scope.vars.currentTab !== '' && !confirm(message)) { 110 | event.preventDefault(); 111 | } 112 | }); 113 | 114 | /** 115 | * Save tabs session 116 | */ 117 | const saveSession = () => { 118 | if ($scope.vars.saveTabs) { 119 | const tabs = $scope.vars.tabs.map((tab) => { 120 | return { 121 | name: tab.name, 122 | sql: tab.sql, 123 | buttonTitle: tab.buttonTitle, 124 | format: tab.format, 125 | results: [], 126 | editor: null, 127 | selectedResultTab: 0 128 | }; 129 | }); 130 | localStorageService.set(SQL_SESSION_KEY, tabs); 131 | } 132 | } 133 | 134 | /** 135 | * @ngdoc method 136 | * @methodOf smi2.controller:SqlController 137 | * @name executeQuery 138 | * @description Execute single SQL query 139 | * @param {} query 140 | */ 141 | $scope.executeQuery = (query, queue, resultContainer) => { 142 | 143 | // содержит дополнительные GET параметры для выполнения запрос 144 | let extendSettings = ''; 145 | 146 | $scope.vars.currentTab.loading = true; 147 | 148 | // если указан limitRows 149 | if ($scope.vars.limitRows) { 150 | extendSettings += 'max_result_rows=' + $scope.vars.limitRows + '&result_overflow_mode=throw'; 151 | } 152 | 153 | API.query(query.sql, query.format, true, extendSettings).then((data) => { 154 | 155 | let r = data; 156 | 157 | if (typeof data !== 'object') { 158 | data = { 159 | data: r, 160 | meta: null, 161 | rows: null, 162 | statistics: null 163 | }; 164 | } 165 | data.error = false; 166 | data.query = query; 167 | let resultData = $scope.renderResult(data); 168 | resultContainer.data.push(resultData); 169 | 170 | // Рекурсивный вызов executeQuery если в очереди 171 | // еще остались элементы 172 | if ((query.index + 1) < queue.length) { 173 | $scope.executeQuery(queue[query.index + 1], queue, resultContainer); 174 | } 175 | else { 176 | // отрисовка 177 | $scope.renderFinalResult(resultContainer); 178 | } 179 | 180 | }, (response) => { 181 | $mdToast.show( 182 | $mdToast 183 | .simple() 184 | .content($filter('translate')('Ошибка')) 185 | .theme(ThemeService.theme) 186 | .position('bottom right') 187 | ); 188 | 189 | let result = { 190 | meta: null, 191 | rows: null, 192 | query: query, 193 | statistics: null 194 | }; 195 | if (response && response.data) { 196 | result.error = angular.toJson(response.data).replace(/\\n/gi, '
').replace(/^"/, '').replace(/"$/, ''); 197 | } else { 198 | result.error = response; 199 | } 200 | resultContainer.data.push($scope.renderResult(result)); 201 | 202 | $scope.renderFinalResult(resultContainer); 203 | }); 204 | }; 205 | 206 | /** 207 | * Show results 208 | * @param data 209 | * @returns {*} 210 | */ 211 | $scope.renderResult = (data) => { 212 | if (typeof data.error == 'string') { 213 | data.result = '
' + data.error + '
'; 214 | } 215 | else if (typeof data.data !== 'object') { 216 | if (typeof data.data !== 'string') { 217 | data.result = '
' + angular.toJson(data.data, true) + '
'; 218 | } 219 | else { 220 | data.result = '
' + data.data + '
'; 221 | } 222 | } 223 | else { 224 | data.result = API.dataToHtml(data); 225 | data.createtable = API.dataToCreateTable(data); 226 | } 227 | return data; 228 | }; 229 | 230 | /** 231 | * Operations after render 232 | * @param result 233 | */ 234 | $scope.renderFinalResult = (result) => { 235 | $scope.vars.currentTab.loading = false; 236 | if (result.data.find((item) => ( 237 | item.query && 238 | item.query.keyword && 239 | ['DROP', 'CREATE', 'ALTER'].indexOf( 240 | item.query.keyword.toUpperCase() 241 | ) != -1 242 | ))) { 243 | $scope.selectDatabase($scope.vars.db); 244 | } 245 | }; 246 | 247 | /** 248 | * Execute SQL statement 249 | * @param type 250 | */ 251 | $scope.execute = (type, tab) => { 252 | 253 | let sql = tab.sql; 254 | let numquery = 0; 255 | const editor = tab.editor; 256 | let queue = []; 257 | let selectSql = editor.getSelectedText(); 258 | let result = { 259 | data: [], 260 | time: $filter('date')(new Date(), 'HH:mm:ss'), 261 | pinned: false 262 | }; 263 | 264 | // получаем выделенный текст, и если выделено - ранаем выделение 265 | if (!(selectSql === '' || selectSql === null)) { 266 | sql = selectSql; 267 | } 268 | 269 | // Выход если пустой sql 270 | if (sql === '' || sql === null) { 271 | $mdToast.show( 272 | $mdToast 273 | .simple() 274 | .content($filter('translate')('Не введен SQL')) 275 | .theme(ThemeService.theme) 276 | .position('bottom right') 277 | ); 278 | 279 | return; 280 | } 281 | 282 | // Clear not-pinned tabs 283 | tab.results = tab.results.reduce((arr, item) => { 284 | if (item.pinned) { 285 | arr.push(item); 286 | } 287 | return arr; 288 | }, []); 289 | tab.results.unshift(result); 290 | 291 | // Save to SQL log 292 | if ($scope.vars.sqlLog.indexOf(sql) == -1) { 293 | $scope.vars.sqlLog.unshift(sql); 294 | if ($scope.vars.sqlLog.length > SQL_LOG_LENGTH) { 295 | $scope.vars.sqlLog.splice(0, SQL_LOG_LENGTH); 296 | } 297 | localStorageService.set(SQL_LOG_KEY, $scope.vars.sqlLog); 298 | } 299 | 300 | // Save tabs session 301 | saveSession(); 302 | 303 | // Split SQL into subqueries 304 | editor 305 | .session 306 | .$mode 307 | .splitByTokens(sql, 'constant.character.escape', ';;') 308 | .forEach((item) => { 309 | 310 | const subSql = item.sql; 311 | 312 | // Ignore short queries 313 | if (subSql.length < 5) { 314 | return; 315 | } 316 | 317 | // Если комманда исполнить текущий и НЕ выделен текст -> пропускаем все пока не найдем подходящий 318 | if (type == 'current' && !selectSql) { 319 | let cursor = editor.selection.getCursor(); 320 | 321 | if (!cursor || !angular.isDefined(cursor) || 322 | item.range.compare(cursor.row, cursor.column) !== 0) { 323 | return; 324 | } 325 | } 326 | 327 | let _format = null; 328 | let _format_seted = false; 329 | let storage = false; 330 | let _keyword = null; 331 | let set_format = editor.session.$mode.findTokens(subSql, "storage", true); 332 | let keyword = editor.session.$mode.findTokens(subSql, "keyword", true); 333 | 334 | if (set_format.hasOwnProperty('value')) { 335 | _format = false; 336 | storage = set_format.value; 337 | } 338 | else { 339 | _format = ' FORMAT JSON '; 340 | _format_seted = true; 341 | } 342 | if (keyword.hasOwnProperty('value')) { 343 | _keyword = keyword.value; 344 | } 345 | if (_keyword !== 'select') { 346 | _format = false; 347 | _format_seted = false; 348 | } 349 | 350 | // Queue creation 351 | queue.push({ 352 | sql: subSql, 353 | index: numquery, 354 | format: _format, 355 | setedformat: _format_seted, 356 | keyword: _keyword, 357 | storage: storage 358 | }); 359 | 360 | numquery++; 361 | }); 362 | 363 | if (queue.length) { 364 | $scope.executeQuery(queue[0], queue, result); 365 | } 366 | 367 | }; 368 | 369 | /** 370 | * Set theme for all editors 371 | * @param theme 372 | */ 373 | $scope.setTheme = (theme) => { 374 | $scope.vars.theme = theme; 375 | $scope.vars.tabs.forEach((tab) => tab.editor.setTheme('ace/theme/' + theme)); 376 | localStorageService.set('editorTheme', theme); 377 | }; 378 | 379 | /** 380 | * Set database name 381 | * @param db 382 | */ 383 | $scope.selectDatabase = (db) => { 384 | 385 | if (!db) { 386 | return; 387 | } 388 | 389 | $scope.vars.db = db; 390 | API.setDatabase(db); 391 | 392 | API.query("SELECT table,name,type FROM system.columns WHERE database=\'" + db + "\'", null) 393 | .then((data) => { 394 | 395 | let fields = [], 396 | ufields = {}; 397 | let tables = [], 398 | utables = {}; 399 | let keys = []; 400 | data.meta.forEach((cell) => { 401 | keys.push(cell.name); 402 | }); 403 | data.data.forEach((row) => { 404 | keys.forEach((key) => { 405 | 406 | if (key == 'table') { 407 | if (!utables.hasOwnProperty(row[key])) { 408 | utables[row[key]] = 1; 409 | tables.push(row[key]); 410 | } 411 | } 412 | if (key == 'name') { 413 | if (!ufields.hasOwnProperty(row[key])) { 414 | ufields[row[key]] = 1; 415 | fields.push(row[key]); 416 | } 417 | } 418 | }); 419 | }); 420 | 421 | window.global_keywords_fields = fields.join('|') + '|'; 422 | window.global_keywords_tables = tables.join('|') + '|' + db; 423 | 424 | // reload highlights 425 | $scope.vars.tabs.forEach((tab) => { 426 | if (tab.editor) { 427 | tab.editor.session.setMode({ 428 | path: "ace/mode/clickhouse", 429 | v: Date.now() 430 | }); 431 | tab.editor.session.bgTokenizer.start(0); 432 | } 433 | }); 434 | }); 435 | }; 436 | 437 | /** 438 | * Add word from dict to SQL etitor 439 | */ 440 | $scope.addDictionariesWord = (word) => { 441 | const editor = $scope.vars.currentTab.editor; 442 | const position = editor.getCursorPosition(); 443 | position.column += word.length; 444 | editor.clearSelection(); 445 | editor.insert(word); 446 | $scope.vars.currentTab.sql = editor.getValue(); 447 | $timeout(() => { 448 | editor.focus(); 449 | editor.moveCursorToPosition(position); 450 | }); 451 | }; 452 | 453 | /** 454 | * Load dicts for ACE autocomplete 455 | */ 456 | $scope.loadDictionaries = () => { 457 | $scope.vars.dictionaries = []; 458 | API.query("select name,key,attribute.names,attribute.types from system.dictionaries ARRAY JOIN attribute ORDER BY name,attribute.names", null).then((data) => { 459 | data.data.forEach((item) => { 460 | // dictGetUInt64('ads.x', 'site_id', toUInt64(xxxx)) AS site_id, 461 | let dic = 'dictGet' + item["attribute.types"] + '(\'' + item.name + '\',\'' + item["attribute.names"] + '\',to' + item.key + '( ID ) ) AS ' + item.name.replace(/\./, '_') + '_' + item["attribute.names"] + ','; 462 | 463 | $scope.vars.dictionaries.push({ 464 | dic: dic, 465 | title: item.name + '.' + item["attribute.names"] + ' as ' + item["attribute.types"] 466 | }); 467 | }); 468 | }); 469 | }; 470 | 471 | /** 472 | * ACE editor init on creation 473 | * @param editor 474 | */ 475 | $scope.aceLoaded = (editor) => { 476 | let tab = $scope.vars.tabs.find(tab => !tab.editor) || $scope.vars.currentTab; 477 | tab.editor = editor; 478 | editor.$blockScrolling = Infinity; 479 | 480 | // Load settings from LocalStorage 481 | editor.setOptions({ 482 | fontSize: $scope.vars.fontSize + 'px', 483 | enableBasicAutocompletion : true, 484 | enableSnippet:true, 485 | enableLiveAutocompletion:false 486 | }); 487 | editor.setTheme('ace/theme/' + $scope.vars.theme); 488 | 489 | // reload keywords & highlights 490 | editor.session.setMode({ 491 | path: "ace/mode/clickhouse", 492 | v: Date.now() 493 | }); 494 | editor.session.bgTokenizer.start(0); 495 | 496 | // @todo:Кастомный cmd+enter чтобы ранать Все/или выделенное 497 | editor.commands.addCommand({ 498 | name: 'runCurrentCommand', 499 | bindKey: { 500 | win: 'Ctrl-Enter', 501 | mac: 'Command-Enter' 502 | }, 503 | exec: () => { 504 | $scope.execute('current', tab); 505 | } 506 | }); 507 | 508 | editor.commands.addCommand({ 509 | name: 'runAllCommand', 510 | bindKey: { 511 | win: 'Shift-Ctrl-Enter', 512 | mac: 'Shift-Command-Enter' 513 | }, 514 | exec: () => { 515 | $scope.execute('all', tab); 516 | } 517 | }); 518 | 519 | editor.clearSelection(); 520 | editor.focus(); 521 | editor.selection.moveTo(0, 0); 522 | 523 | // Повесить эвент и переиминовывать кнопку -"Выполнить" 524 | editor.on('changeSelection', () => { 525 | $timeout(() => { 526 | tab.buttonTitle = editor.getSelectedText() !== '' ? $filter('translate')('Выполнить выделенное ⌘ + ⏎') : $filter('translate')('Выполнить все ⇧ + ⌘ + ⏎'); 527 | if (tab.originalSql) { 528 | tab.changed = (tab.originalSql != tab.sql); 529 | } 530 | }); 531 | }); 532 | 533 | $scope.loadDictionaries(); 534 | }; 535 | 536 | /** 537 | * Watch for menu database changes 538 | */ 539 | $rootScope.$watch('currentDatabase', $scope.selectDatabase); 540 | 541 | /** 542 | * Watch and save settings in LocalStorage 543 | */ 544 | $scope.$watch('vars.limitRows', (curr) => localStorageService.set('editorLimitRows', curr)); 545 | 546 | /** 547 | * Watch and save settings in LocalStorage 548 | */ 549 | $scope.$watch('vars.fontSize', (fontSize) => { 550 | if (fontSize) { 551 | $scope.vars.tabs.forEach((tab) => tab.editor && tab.editor.setOptions({ 552 | fontSize: fontSize + 'px' 553 | })); 554 | localStorageService.set('editorFontSize', fontSize); 555 | } 556 | }); 557 | 558 | /** 559 | * Save SQL to history 560 | * @param tab 561 | * @param ev 562 | */ 563 | $scope.save = (tab, ev) => $mdDialog.show( 564 | $mdDialog.prompt() 565 | .title($filter('translate')('Сохранить SQL как')) 566 | .placeholder($filter('translate')('название')) 567 | .initialValue(tab.name) 568 | .targetEvent(ev) 569 | .ok($filter('translate')('Сохранить')) 570 | .cancel($filter('translate')('Отмена')) 571 | ).then((name)=> { 572 | const index = $scope.vars.sqlHistory.findIndex((item) => (item.name == tab.name)); 573 | if (index != -1) { 574 | $scope.vars.sqlHistory[index].sql = tab.sql; 575 | $scope.vars.sqlHistory[index].name = name; 576 | } else { 577 | $scope.vars.sqlHistory.push({ 578 | sql: tab.sql, 579 | name 580 | }); 581 | } 582 | tab.originalSql = tab.sql; 583 | tab.name = name; 584 | localStorageService.set(SQL_HISTORY_KEY, $scope.vars.sqlHistory); 585 | saveSession(); 586 | }); 587 | 588 | /** 589 | * Load SQL from history 590 | * @param history 591 | */ 592 | $scope.load = (history) => { 593 | $scope.vars.currentTab.sql = history.sql; 594 | $scope.vars.currentTab.originalSql = history.sql; 595 | $scope.vars.currentTab.name = history.name; 596 | }; 597 | 598 | /** 599 | * Create export data 600 | * @param result 601 | * @returns {number[]} 602 | */ 603 | $scope.getExportData = result => ( 604 | result.map(item => Object.keys(item).map(key => ( 605 | angular.isArray(item[key]) ? item[key].join(', ') : item[key] 606 | ))) 607 | ); 608 | 609 | $scope.getExportHeaders = result => result.map(item => item.name); 610 | 611 | /** 612 | * Inserting new SQL tab 613 | */ 614 | $scope.addTab = () => { 615 | $scope.vars.currentTab = { 616 | name: 'new SQL', 617 | sql: '', 618 | buttonTitle: $filter('translate')('Выполнить ⌘ + ⏎'), 619 | format: {}, 620 | editor: null, 621 | results: [], 622 | selectedResultTab: 0 623 | }; 624 | $scope.vars.tabs.push($scope.vars.currentTab); 625 | saveSession(); 626 | }; 627 | 628 | /** 629 | * Remove SQL tab 630 | * @param tab 631 | * @param event 632 | */ 633 | $scope.removeTab = (tab, event) => { 634 | event.stopPropagation(); 635 | 636 | const clear = () => { 637 | $scope.vars.tabs.splice($scope.vars.tabs.indexOf(tab), 1); 638 | if ($scope.vars.tabs.length == $scope.vars.selectedTab) { 639 | $scope.vars.selectedTab--; 640 | } 641 | }; 642 | 643 | if (tab.changed) { 644 | $mdDialog.show( 645 | $mdDialog.confirm() 646 | .title($filter('translate')('SQL изменен. Сохранить перед закрытием?')) 647 | .targetEvent(event) 648 | .ok($filter('translate')('Да')) 649 | .cancel($filter('translate')('Нет')) 650 | ).then(()=> { 651 | const index = $scope.vars.sqlHistory.findIndex((item) => (item.name == tab.name)); 652 | if (index != -1) { 653 | $scope.vars.sqlHistory[index].sql = tab.sql; 654 | $scope.vars.sqlHistory[index].name = tab.name; 655 | } 656 | localStorageService.set(SQL_HISTORY_KEY, $scope.vars.sqlHistory); 657 | clear(); 658 | }, clear); 659 | } else { 660 | clear(); 661 | } 662 | }; 663 | 664 | /** 665 | * Remove result Tab 666 | * @param tab 667 | * @param result 668 | * @param $event 669 | */ 670 | $scope.removeResult = (tab, result, event) => { 671 | event.stopPropagation(); 672 | tab.results.splice(tab.results.indexOf(result), 1); 673 | }; 674 | 675 | /** 676 | * Toggle settings panel 677 | */ 678 | $scope.toggleSidenav = (id) => { 679 | $mdSidenav(id).toggle(); 680 | }; 681 | 682 | /** 683 | * Remove history item 684 | * @param item 685 | * @param event 686 | */ 687 | $scope.removeHistory = (item, event) => { 688 | event.preventDefault(); 689 | const index = $scope.vars.sqlHistory.indexOf(item); 690 | if (index != -1) { 691 | $mdDialog.show( 692 | $mdDialog.confirm() 693 | .title(`Удалить ${item.name}?`) 694 | .targetEvent(event) 695 | .ok('Да') 696 | .cancel('Нет') 697 | ).then(()=> { 698 | $scope.vars.sqlHistory.splice(index, 1); 699 | localStorageService.set(SQL_HISTORY_KEY, $scope.vars.sqlHistory); 700 | }); 701 | } 702 | }; 703 | 704 | /** 705 | * Save session checkbox state in LS 706 | */ 707 | $scope.$watch('vars.saveTabs', (value, old) => { 708 | localStorageService.set(SQL_SAVE_TABS_KEY, value); 709 | if (old === false && value === true) { 710 | saveSession(); 711 | } 712 | }); 713 | 714 | /** 715 | * Change UI themetam_tam641 716 | * @param theme 717 | */ 718 | $scope.setUiTheme = (theme) => ThemeService.set(theme.name); 719 | 720 | /** 721 | * Set SQL in editor 722 | * @param sql 723 | */ 724 | $scope.setSql = (sql) => { 725 | $scope.vars.currentTab.sql = sql; 726 | $scope.toggleSidenav('log'); 727 | $timeout(() => $scope.vars.currentTab.editor.focus(), 500); 728 | }; 729 | 730 | /** 731 | * Rename tab 732 | * @param tab 733 | * @param event 734 | */ 735 | $scope.changeTabName = (tab, event) => { 736 | event.stopPropagation(); 737 | $mdDialog.show( 738 | $mdDialog.prompt() 739 | .title($filter('translate')('Название вкладки')) 740 | .placeholder($filter('translate')('название')) 741 | .initialValue(tab.name) 742 | .targetEvent(event) 743 | .ok($filter('translate')('Применить')) 744 | .cancel($filter('translate')('Отмена')) 745 | ).then((name)=> { 746 | const index = $scope.vars.sqlHistory.findIndex((item) => (item.name == tab.name)); 747 | if (index != -1) { 748 | tab.originalSql = tab.sql; 749 | $scope.vars.sqlHistory[index].name = name; 750 | localStorageService.set(SQL_HISTORY_KEY, $scope.vars.sqlHistory); 751 | } 752 | tab.name = name; 753 | saveSession(); 754 | }); 755 | }; 756 | 757 | /** 758 | * Init :) 759 | */ 760 | if ($scope.vars.saveTabs) { 761 | $scope.vars.tabs = localStorageService.get(SQL_SESSION_KEY); 762 | if ($scope.vars.tabs.length > 0) { 763 | $timeout(() => ($scope.vars.currentTab = $scope.vars.tabs[0]), 500); 764 | } else { 765 | $scope.addTab(); 766 | } 767 | } else { 768 | localStorageService.set(SQL_SESSION_KEY, []); 769 | $scope.addTab(); 770 | } 771 | if ($rootScope.currentDatabase) { 772 | $scope.selectDatabase($rootScope.currentDatabase); 773 | }; 774 | 775 | /** 776 | * Controller destructor 777 | */ 778 | $scope.$on('$destroy', () => { 779 | API.setDatabase($rootScope.currentDatabase); 780 | clearRouterListener(); 781 | $window.onbeforeunload = null; 782 | }); 783 | } 784 | })(angular, smi2); 785 | --------------------------------------------------------------------------------