├── .dockerignore ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── composer.json ├── composer.lock ├── docker-compose.yml ├── php └── Job │ ├── Admin │ └── Configuration.php │ ├── Database │ ├── Execute.php │ ├── Info.php │ ├── Job.php │ └── Spaces.php │ ├── Export │ └── Csv.php │ ├── Row │ ├── Create.php │ ├── Job.php │ ├── Remove.php │ └── Update.php │ └── Space │ ├── AddProperty.php │ ├── Create.php │ ├── CreateIndex.php │ ├── Drop.php │ ├── Info.php │ ├── Job.php │ ├── RemoveIndex.php │ ├── RemoveProperty.php │ ├── Select.php │ └── Truncate.php ├── public ├── .htaccess ├── admin │ ├── 39156475-8b873e18-4756-11e8-89d0-6ffca592f664.png │ ├── index.php │ ├── js │ │ ├── Database │ │ │ ├── Info.js │ │ │ ├── Query.js │ │ │ ├── Spaces.js │ │ │ └── Tab.js │ │ ├── Home │ │ │ ├── Connections.js │ │ │ ├── New.js │ │ │ └── Tab.js │ │ ├── Space │ │ │ ├── Collection.js │ │ │ ├── Format.js │ │ │ ├── Indexes.js │ │ │ ├── Info.js │ │ │ ├── Tab.js │ │ │ └── toolbar │ │ │ │ ├── Collection.js │ │ │ │ └── Search.js │ │ ├── Viewport.js │ │ ├── bootstrap.js │ │ ├── data │ │ │ └── proxy │ │ │ │ └── PagingDispatch.js │ │ ├── field │ │ │ └── Filter.js │ │ └── overrides │ │ │ └── Toolbar.js │ └── style.css ├── index.php └── server.php └── var └── .gitkeep /.dockerignore: -------------------------------------------------------------------------------- 1 | **/*.log 2 | **/*.md 3 | **/*.php~ 4 | **/._* 5 | **/.dockerignore 6 | **/.DS_Store 7 | **/.gitignore 8 | **/Dockerfile 9 | **/Thumbs.db 10 | var/ 11 | vendor/ 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /var 2 | /vendor 3 | /public/admin/downloads 4 | /public/admin/ext-6.2.0/ 5 | /public/admin/fontawesome-free-5.0.6/ 6 | /composer.phar 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build 2 | 3 | FROM php:apache AS build 4 | 5 | WORKDIR /build 6 | 7 | RUN curl -sSLf \ 8 | -o /usr/local/bin/install-php-extensions \ 9 | https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions && \ 10 | chmod +x /usr/local/bin/install-php-extensions && \ 11 | install-php-extensions gd xdebug 12 | 13 | RUN apt-get update && apt-get install -y git wget zip 14 | RUN install-php-extensions decimal 15 | 16 | RUN wget -q https://use.fontawesome.com/releases/v5.0.6/fontawesome-free-5.0.6.zip \ 17 | && wget -q http://cdn.sencha.com/ext/gpl/ext-6.2.0-gpl.zip \ 18 | && unzip -q ./fontawesome-free-5.0.6.zip \ 19 | && unzip -q ./ext-6.2.0-gpl.zip 20 | 21 | COPY .git .git/ 22 | RUN CI_COMMIT_TAG=$(git describe --tags) \ 23 | CI_COMMIT_REF_NAME=$(git rev-parse --abbrev-ref HEAD) \ 24 | CI_COMMIT_SHA=$(git rev-parse --verify HEAD) \ 25 | CI_COMMIT_SHORT_SHA=$(git rev-parse --verify HEAD | head -c 8) \ 26 | && echo " '$CI_COMMIT_TAG', 'sha' => '$CI_COMMIT_SHA', 'short_sha' => '$CI_COMMIT_SHORT_SHA','ref_name'=>'$CI_COMMIT_REF_NAME'];" > version.php 27 | 28 | COPY php php/ 29 | COPY composer.json composer.lock ./ 30 | 31 | COPY --from=composer:latest /usr/bin/composer /usr/bin/composer 32 | RUN composer install --prefer-dist --no-dev --no-autoloader --no-scripts --no-progress \ 33 | && composer dump-autoload --classmap-authoritative --no-dev \ 34 | && composer clear-cache 35 | 36 | 37 | # Runtime 38 | FROM php:apache 39 | 40 | WORKDIR /var/www/html 41 | 42 | RUN curl -sSLf \ 43 | -o /usr/local/bin/install-php-extensions \ 44 | https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions && \ 45 | chmod +x /usr/local/bin/install-php-extensions && \ 46 | install-php-extensions gd xdebug 47 | 48 | RUN install-php-extensions decimal 49 | 50 | RUN apt-get update && apt-get install -y zip zlib1g-dev libzip-dev uuid-dev \ 51 | && docker-php-ext-install zip opcache \ 52 | && pecl install uuid \ 53 | && docker-php-ext-enable uuid \ 54 | && a2enmod rewrite 55 | 56 | RUN echo "ServerName tarantool-admin" > /etc/apache2/conf-enabled/server-name.conf 57 | RUN sed -i 's~DocumentRoot.*$~DocumentRoot /var/www/html/public~' /etc/apache2/sites-available/000-default.conf 58 | 59 | RUN mkdir -p public/admin/downloads \ 60 | && chown www-data public/admin/downloads \ 61 | && chgrp www-data public/admin/downloads 62 | 63 | RUN mkdir var \ 64 | && chown www-data var \ 65 | && chgrp www-data var 66 | 67 | COPY php php/ 68 | COPY public public/ 69 | 70 | COPY --from=build /build/fontawesome-free-5.0.6/on-server public/admin/fontawesome-free-5.0.6 71 | COPY --from=build /build/ext-6.2.0/build/ext-all.js public/admin/ext-6.2.0/ext-all.js 72 | COPY --from=build /build/ext-6.2.0/build/classic/theme-crisp public/admin/ext-6.2.0/classic/theme-crisp 73 | COPY --from=build /build/vendor vendor/ 74 | COPY --from=build /build/version.php var/ 75 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-2024 Basis IT 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included 12 | in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tarantool admin 2 | This application can be used to manage schema and data in tarantool database using web gui. 3 | Feel free to contribute any way. 4 | 5 | ## Running existing build [![Docker Repository on Quay](https://quay.io/repository/basis-company/tarantool-admin/status "Docker Repository on Quay")](https://quay.io/repository/basis-company/tarantool-admin) 6 | Run `docker run -p 8000:80 quay.io/basis-company/tarantool-admin` 7 | Open [http://localhost:8000](http://localhost:8000) in your browser. 8 | 9 | ## Configure using env 10 | Application can be configured via environment: 11 | * TARANTOOL_CHECK_VERSION - default is `true`. set to `false` if you want to disable version check 12 | * TARANTOOL_CONNECT_TIMEOUT - connect timeout 13 | * TARANTOOL_CONNECTIONS - comma-separated connection strings 14 | * TARANTOOL_CONNECTIONS_READONLY - disable connections editor 15 | * TARANTOOL_DATABASE_QUERY - enable Query database tab 16 | * TARANTOOL_ENABLE_VINYL_PAGE_COUNT - if your vinyl spaces are not to large, you can enable index:count requests 17 | * TARANTOOL_READONLY - disable any database changes 18 | * TARANTOOL_SOCKET_TIMEOUT - connection read/write timeout 19 | * TARANTOOL_TCP_NODELAY - disable Nagle TCP algorithm 20 | 21 | ## You can build image yourself. 22 | * Clone repository: `git clone https://github.com/basis-company/tarantool-admin.git` 23 | * Change current directory: `cd tarantool-admin` 24 | * Run `docker build .` 25 | 26 | ## Youtube demo 27 | Short demo of ui is available on youtube: 28 | 29 | Short demo 30 | 31 | ## Development 32 | 33 | * Install git and docker 34 | * Clone repository: `git clone https://github.com/basis-company/tarantool-admin.git` 35 | * Change current directory: `cd tarantool-admin` 36 | * Run developer environment using `docker-compose up -d` 37 | * Access environment using http://0.0.0.0:8888 38 | * Use "tarantool" hostname configuration with form default values: 39 | * port 3301 40 | * username guest 41 | * password (should be empty) 42 | * Use your favorite ide to edit php/js, all code will be updated on the fly 43 | * Follow https://phptherightway.com/ recommendations 44 | * Don't repeat yourself 45 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basis-company/tarantool-admin", 3 | "description": "A web interface for Tarantool.", 4 | "license": "MIT", 5 | "authors": [{ 6 | "name": "Dmitry Krokhin", 7 | "email": "nekufa@gmail.com" 8 | }], 9 | "require": { 10 | "php": "^8", 11 | "symfony/uid": "^6.1.3", 12 | "tarantool/mapper": "^6.1.1" 13 | }, 14 | "suggest": { 15 | "ext-decimal": "For using decimals with Tarantool 2.3+" 16 | }, 17 | "autoload": { 18 | "psr-4": { 19 | "": "php/" 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "4418aa70951ebd6d75f1be0989c85e73", 8 | "packages": [ 9 | { 10 | "name": "psr/cache", 11 | "version": "2.0.0", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/php-fig/cache.git", 15 | "reference": "213f9dbc5b9bfbc4f8db86d2838dc968752ce13b" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/php-fig/cache/zipball/213f9dbc5b9bfbc4f8db86d2838dc968752ce13b", 20 | "reference": "213f9dbc5b9bfbc4f8db86d2838dc968752ce13b", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": ">=8.0.0" 25 | }, 26 | "type": "library", 27 | "extra": { 28 | "branch-alias": { 29 | "dev-master": "1.0.x-dev" 30 | } 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "Psr\\Cache\\": "src/" 35 | } 36 | }, 37 | "notification-url": "https://packagist.org/downloads/", 38 | "license": [ 39 | "MIT" 40 | ], 41 | "authors": [ 42 | { 43 | "name": "PHP-FIG", 44 | "homepage": "https://www.php-fig.org/" 45 | } 46 | ], 47 | "description": "Common interface for caching libraries", 48 | "keywords": [ 49 | "cache", 50 | "psr", 51 | "psr-6" 52 | ], 53 | "support": { 54 | "source": "https://github.com/php-fig/cache/tree/2.0.0" 55 | }, 56 | "time": "2021-02-03T23:23:37+00:00" 57 | }, 58 | { 59 | "name": "rybakit/msgpack", 60 | "version": "v0.9.1", 61 | "source": { 62 | "type": "git", 63 | "url": "https://github.com/rybakit/msgpack.php.git", 64 | "reference": "fc6bc45e92274e78c32d0a86f2e2cc1f8b5e017b" 65 | }, 66 | "dist": { 67 | "type": "zip", 68 | "url": "https://api.github.com/repos/rybakit/msgpack.php/zipball/fc6bc45e92274e78c32d0a86f2e2cc1f8b5e017b", 69 | "reference": "fc6bc45e92274e78c32d0a86f2e2cc1f8b5e017b", 70 | "shasum": "" 71 | }, 72 | "require": { 73 | "php": "^7.1.1|^8" 74 | }, 75 | "require-dev": { 76 | "ext-gmp": "*", 77 | "friendsofphp/php-cs-fixer": "^2.14", 78 | "phpunit/phpunit": "^7.1|^8|^9", 79 | "vimeo/psalm": "^3.9|^4" 80 | }, 81 | "suggest": { 82 | "ext-decimal": "For converting overflowed integers to Decimal objects", 83 | "ext-gmp": "For converting overflowed integers to GMP objects" 84 | }, 85 | "type": "library", 86 | "autoload": { 87 | "psr-4": { 88 | "MessagePack\\": "src/" 89 | } 90 | }, 91 | "notification-url": "https://packagist.org/downloads/", 92 | "license": [ 93 | "MIT" 94 | ], 95 | "authors": [ 96 | { 97 | "name": "Eugene Leonovich", 98 | "email": "gen.work@gmail.com" 99 | } 100 | ], 101 | "description": "A pure PHP implementation of the MessagePack serialization format.", 102 | "keywords": [ 103 | "messagepack", 104 | "msgpack", 105 | "pure", 106 | "streaming" 107 | ], 108 | "support": { 109 | "issues": "https://github.com/rybakit/msgpack.php/issues", 110 | "source": "https://github.com/rybakit/msgpack.php/tree/v0.9.1" 111 | }, 112 | "funding": [ 113 | { 114 | "url": "https://github.com/rybakit", 115 | "type": "github" 116 | } 117 | ], 118 | "time": "2022-02-16T00:48:07+00:00" 119 | }, 120 | { 121 | "name": "symfony/polyfill-uuid", 122 | "version": "v1.30.0", 123 | "source": { 124 | "type": "git", 125 | "url": "https://github.com/symfony/polyfill-uuid.git", 126 | "reference": "2ba1f33797470debcda07fe9dce20a0003df18e9" 127 | }, 128 | "dist": { 129 | "type": "zip", 130 | "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/2ba1f33797470debcda07fe9dce20a0003df18e9", 131 | "reference": "2ba1f33797470debcda07fe9dce20a0003df18e9", 132 | "shasum": "" 133 | }, 134 | "require": { 135 | "php": ">=7.1" 136 | }, 137 | "provide": { 138 | "ext-uuid": "*" 139 | }, 140 | "suggest": { 141 | "ext-uuid": "For best performance" 142 | }, 143 | "type": "library", 144 | "extra": { 145 | "thanks": { 146 | "name": "symfony/polyfill", 147 | "url": "https://github.com/symfony/polyfill" 148 | } 149 | }, 150 | "autoload": { 151 | "files": [ 152 | "bootstrap.php" 153 | ], 154 | "psr-4": { 155 | "Symfony\\Polyfill\\Uuid\\": "" 156 | } 157 | }, 158 | "notification-url": "https://packagist.org/downloads/", 159 | "license": [ 160 | "MIT" 161 | ], 162 | "authors": [ 163 | { 164 | "name": "Grégoire Pineau", 165 | "email": "lyrixx@lyrixx.info" 166 | }, 167 | { 168 | "name": "Symfony Community", 169 | "homepage": "https://symfony.com/contributors" 170 | } 171 | ], 172 | "description": "Symfony polyfill for uuid functions", 173 | "homepage": "https://symfony.com", 174 | "keywords": [ 175 | "compatibility", 176 | "polyfill", 177 | "portable", 178 | "uuid" 179 | ], 180 | "support": { 181 | "source": "https://github.com/symfony/polyfill-uuid/tree/v1.30.0" 182 | }, 183 | "funding": [ 184 | { 185 | "url": "https://symfony.com/sponsor", 186 | "type": "custom" 187 | }, 188 | { 189 | "url": "https://github.com/fabpot", 190 | "type": "github" 191 | }, 192 | { 193 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 194 | "type": "tidelift" 195 | } 196 | ], 197 | "time": "2024-05-31T15:07:36+00:00" 198 | }, 199 | { 200 | "name": "symfony/uid", 201 | "version": "v6.4.8", 202 | "source": { 203 | "type": "git", 204 | "url": "https://github.com/symfony/uid.git", 205 | "reference": "35904eca37a84bb764c560cbfcac9f0ac2bcdbdf" 206 | }, 207 | "dist": { 208 | "type": "zip", 209 | "url": "https://api.github.com/repos/symfony/uid/zipball/35904eca37a84bb764c560cbfcac9f0ac2bcdbdf", 210 | "reference": "35904eca37a84bb764c560cbfcac9f0ac2bcdbdf", 211 | "shasum": "" 212 | }, 213 | "require": { 214 | "php": ">=8.1", 215 | "symfony/polyfill-uuid": "^1.15" 216 | }, 217 | "require-dev": { 218 | "symfony/console": "^5.4|^6.0|^7.0" 219 | }, 220 | "type": "library", 221 | "autoload": { 222 | "psr-4": { 223 | "Symfony\\Component\\Uid\\": "" 224 | }, 225 | "exclude-from-classmap": [ 226 | "/Tests/" 227 | ] 228 | }, 229 | "notification-url": "https://packagist.org/downloads/", 230 | "license": [ 231 | "MIT" 232 | ], 233 | "authors": [ 234 | { 235 | "name": "Grégoire Pineau", 236 | "email": "lyrixx@lyrixx.info" 237 | }, 238 | { 239 | "name": "Nicolas Grekas", 240 | "email": "p@tchwork.com" 241 | }, 242 | { 243 | "name": "Symfony Community", 244 | "homepage": "https://symfony.com/contributors" 245 | } 246 | ], 247 | "description": "Provides an object-oriented API to generate and represent UIDs", 248 | "homepage": "https://symfony.com", 249 | "keywords": [ 250 | "UID", 251 | "ulid", 252 | "uuid" 253 | ], 254 | "support": { 255 | "source": "https://github.com/symfony/uid/tree/v6.4.8" 256 | }, 257 | "funding": [ 258 | { 259 | "url": "https://symfony.com/sponsor", 260 | "type": "custom" 261 | }, 262 | { 263 | "url": "https://github.com/fabpot", 264 | "type": "github" 265 | }, 266 | { 267 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 268 | "type": "tidelift" 269 | } 270 | ], 271 | "time": "2024-05-31T14:49:08+00:00" 272 | }, 273 | { 274 | "name": "tarantool/client", 275 | "version": "v0.10.1", 276 | "source": { 277 | "type": "git", 278 | "url": "https://github.com/tarantool-php/client.git", 279 | "reference": "9b0225119deebb263dd8dcaa11b89dfdd109cd73" 280 | }, 281 | "dist": { 282 | "type": "zip", 283 | "url": "https://api.github.com/repos/tarantool-php/client/zipball/9b0225119deebb263dd8dcaa11b89dfdd109cd73", 284 | "reference": "9b0225119deebb263dd8dcaa11b89dfdd109cd73", 285 | "shasum": "" 286 | }, 287 | "require": { 288 | "php": "^7.2.5|^8", 289 | "rybakit/msgpack": "^0.9", 290 | "symfony/uid": "^5.1|^6|^7" 291 | }, 292 | "require-dev": { 293 | "ext-json": "*", 294 | "ext-sockets": "*", 295 | "friendsofphp/php-cs-fixer": "^2.19", 296 | "monolog/monolog": "^1.24|^2.0", 297 | "psr/log": "^1.1", 298 | "tarantool/phpunit-extras": "^0.2.0", 299 | "vimeo/psalm": "^3.9|^4" 300 | }, 301 | "suggest": { 302 | "ext-decimal": "For using decimals with Tarantool 2.3+", 303 | "ext-uuid": "For better performance when using UUIDs with Tarantool 2.4+", 304 | "psr/log": "For using LoggingMiddleware" 305 | }, 306 | "type": "library", 307 | "extra": { 308 | "branch-alias": { 309 | "dev-master": "0.10.x-dev" 310 | } 311 | }, 312 | "autoload": { 313 | "psr-4": { 314 | "Tarantool\\Client\\": "src/" 315 | } 316 | }, 317 | "notification-url": "https://packagist.org/downloads/", 318 | "license": [ 319 | "MIT" 320 | ], 321 | "authors": [ 322 | { 323 | "name": "Eugene Leonovich", 324 | "email": "gen.work@gmail.com" 325 | } 326 | ], 327 | "description": "PHP client for Tarantool.", 328 | "keywords": [ 329 | "client", 330 | "nosql", 331 | "pure", 332 | "tarantool" 333 | ], 334 | "support": { 335 | "issues": "https://github.com/tarantool-php/client/issues", 336 | "source": "https://github.com/tarantool-php/client/tree/v0.10.1" 337 | }, 338 | "funding": [ 339 | { 340 | "url": "https://github.com/rybakit", 341 | "type": "github" 342 | } 343 | ], 344 | "time": "2024-06-20T22:53:05+00:00" 345 | }, 346 | { 347 | "name": "tarantool/mapper", 348 | "version": "6.2.2", 349 | "source": { 350 | "type": "git", 351 | "url": "https://github.com/tarantool-php/mapper.git", 352 | "reference": "b32e9531d508a7d9d679a1915341c24f202674dc" 353 | }, 354 | "dist": { 355 | "type": "zip", 356 | "url": "https://api.github.com/repos/tarantool-php/mapper/zipball/b32e9531d508a7d9d679a1915341c24f202674dc", 357 | "reference": "b32e9531d508a7d9d679a1915341c24f202674dc", 358 | "shasum": "" 359 | }, 360 | "require": { 361 | "php": ">=8.0", 362 | "psr/cache": "^2.0", 363 | "tarantool/client": ">=0.10.0" 364 | }, 365 | "require-dev": { 366 | "monolog/monolog": "^2.9.3", 367 | "phpunit/phpunit": "^9.5", 368 | "symfony/cache": "^5.4.38" 369 | }, 370 | "type": "library", 371 | "autoload": { 372 | "psr-4": { 373 | "Tarantool\\Mapper\\": "src/", 374 | "Tarantool\\Mapper\\Tests\\": "tests/" 375 | } 376 | }, 377 | "notification-url": "https://packagist.org/downloads/", 378 | "license": [ 379 | "MIT" 380 | ], 381 | "authors": [ 382 | { 383 | "name": "Dmitry Krokhin", 384 | "email": "nekufa@gmail.com" 385 | } 386 | ], 387 | "description": "PHP Object Mapper for Tarantool.", 388 | "keywords": [ 389 | "client", 390 | "mapper", 391 | "nosql", 392 | "pure", 393 | "tarantool" 394 | ], 395 | "support": { 396 | "issues": "https://github.com/tarantool-php/mapper/issues", 397 | "source": "https://github.com/tarantool-php/mapper/tree/6.2.2" 398 | }, 399 | "time": "2024-07-10T15:25:25+00:00" 400 | } 401 | ], 402 | "packages-dev": [], 403 | "aliases": [], 404 | "minimum-stability": "stable", 405 | "stability-flags": [], 406 | "prefer-stable": false, 407 | "prefer-lowest": false, 408 | "platform": { 409 | "php": "^8" 410 | }, 411 | "platform-dev": [], 412 | "plugin-api-version": "2.3.0" 413 | } 414 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.5' 2 | 3 | services: 4 | admin: 5 | restart: always 6 | build: 7 | context: . 8 | ports: 9 | - "8888:80" 10 | volumes: 11 | - ./php:/var/www/html/php 12 | - ./public/admin/js:/var/www/html/public/admin/js 13 | - ./public/admin/index.php:/var/www/html/public/admin/index.php 14 | - ./public/admin/style.css:/var/www/html/public/admin/style.css 15 | environment: 16 | - TARANTOOL_TCP_NODELAY=1 17 | - TARANTOOL_DATABASE_QUERY=1 18 | - TARANTOOL_CONNECTIONS=tarantool:3301 19 | depends_on: 20 | - tarantool 21 | 22 | tarantool: 23 | image: "tarantool/tarantool:2" 24 | restart: always 25 | healthcheck: 26 | test: tarantool_is_up 27 | interval: 60s 28 | timeout: 15s 29 | retries: 10 30 | -------------------------------------------------------------------------------- /php/Job/Admin/Configuration.php: -------------------------------------------------------------------------------- 1 | getLatest(); 23 | } 24 | 25 | return [ 26 | 'connectionsReadOnly' => (bool) getenv('TARANTOOL_CONNECTIONS_READONLY'), 27 | 'connections' => explode(',', getenv('TARANTOOL_CONNECTIONS')), 28 | 'query' => (bool) getenv('TARANTOOL_DATABASE_QUERY'), 29 | 'readOnly' => getenv('TARANTOOL_READONLY') == 'true' || getenv('TARANTOOL_READONLY') == '1', 30 | 'version' => $version, 31 | 'latest' => $latest, 32 | ]; 33 | } 34 | 35 | protected function getLatest() 36 | { 37 | $filename = dirname(__DIR__, 3) . '/var/latest.php'; 38 | 39 | if (file_exists($filename)) { 40 | $latest = include $filename; 41 | if ($latest['tag'] && $latest['timestamp'] + $this->ttl >= time()) { 42 | return $latest['tag']; 43 | } 44 | } 45 | 46 | $context = stream_context_create([ 47 | 'http' => [ 48 | 'method' => 'GET', 49 | 'header' => [ 50 | 'User-Agent: PHP', 51 | ] 52 | ] 53 | ]); 54 | 55 | $url = "https://api.github.com/repos/$this->repository/releases/latest"; 56 | $tag = @json_decode(file_get_contents($url, false, $context))->tag_name; 57 | $timestamp = time(); 58 | 59 | $contents = '<' . '?php return ' . var_export(compact('tag', 'timestamp'), true) . ';'; 60 | file_put_contents($filename, $contents); 61 | 62 | return $tag; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /php/Job/Database/Execute.php: -------------------------------------------------------------------------------- 1 | getMapper()->client->evaluate($this->code); 17 | 18 | foreach ($result as $k => $v) { 19 | if (!is_array($v)) { 20 | $result[$k] = ['scalar' => $v]; 21 | } 22 | } 23 | 24 | return [ 25 | 'result' => $result, 26 | 'timing' => 1000 * (microtime(true) - $start), 27 | ]; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /php/Job/Database/Info.php: -------------------------------------------------------------------------------- 1 | getMapper()->client; 12 | 13 | $stats = [ 14 | 'info' => 'box.info', 15 | 'stat' => 'box.stat', 16 | 'slab' => 'box.slab.info', 17 | ]; 18 | 19 | $info = []; 20 | foreach ($stats as $k => $function) { 21 | try { 22 | $info[$k] = $client->call($function)[0]; 23 | } catch (Exception) { 24 | } 25 | } 26 | 27 | return $info; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /php/Job/Database/Job.php: -------------------------------------------------------------------------------- 1 | code), $candidate) !== false) { 37 | $changes = true; 38 | } 39 | } 40 | } 41 | if ($changes) { 42 | throw new Exception("Tarantool admin is in readonly mode"); 43 | } 44 | } 45 | 46 | if (!isset($this->client)) { 47 | if (!$this->hostname || !$this->port) { 48 | if (!$this->socket) { 49 | throw new Exception('Invalid connection parameters'); 50 | } 51 | } 52 | 53 | $dsn = $this->socket ?: 'tcp://' . $this->hostname . ':' . $this->port; 54 | $this->client = Client::fromDsn($dsn)->withMiddleware( 55 | new AuthenticationMiddleware($this->username ?: 'guest', $this->password), 56 | ); 57 | } 58 | 59 | return $this->client; 60 | } 61 | 62 | public function getMapper(): Mapper 63 | { 64 | if (!isset($this->mapper)) { 65 | $this->mapper = new Mapper($this->getClient(), arrays: true); 66 | } 67 | 68 | return $this->mapper; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /php/Job/Database/Spaces.php: -------------------------------------------------------------------------------- 1 | getMapper(); 12 | 13 | $spaces = []; 14 | foreach ($mapper->find('_vspace') as $space) { 15 | try { 16 | if ($space['engine'] !== 'vinyl') { 17 | $space['count'] = $mapper->client->call("box.space." . $space['name'] . ":count")[0]; 18 | } 19 | $space['bsize'] = $mapper->client->call("box.space." . $space['name'] . ":bsize")[0]; 20 | } catch (Exception) { 21 | } 22 | $spaces[] = $space; 23 | } 24 | 25 | return compact('spaces'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /php/Job/Export/Csv.php: -------------------------------------------------------------------------------- 1 | space, 18 | $this->index, 19 | $this->key, 20 | microtime(1), 21 | ])); 22 | 23 | $data = []; 24 | $page = 0; 25 | $this->offset = 0; 26 | 27 | while (!$page || count($data) < $total) { 28 | $result = parent::run(); 29 | foreach ($result['data'] as $item) { 30 | foreach ($item as $k => $v) { 31 | if (is_array($v)) { 32 | $item[$k] = json_encode($v); 33 | } 34 | } 35 | $row = nl2br(implode($this->delimiter, $item)); 36 | $data[] = str_replace(["\n", "\r"], '', $row); 37 | } 38 | 39 | $total = $result['total']; 40 | 41 | $page++; 42 | $this->offset = $page * $this->limit; 43 | } 44 | 45 | $fields = $this->getSpace()->getFields(); 46 | 47 | $contents = implode($this->delimiter, $fields) . PHP_EOL . implode(PHP_EOL, $data); 48 | 49 | $folder = 'admin/downloads'; 50 | 51 | if (!is_dir($folder)) { 52 | if (!mkdir($folder) && !is_dir($folder)) { 53 | throw new RuntimeException(sprintf('Directory "%s" was not created', $folder)); 54 | } 55 | } else { 56 | foreach (scandir($folder) as $file) { 57 | if ($file === '.' || $file === '..') { 58 | continue; 59 | } 60 | $path = $folder . '/' . $file; 61 | if (filemtime($path) < time() - $this->keepFiles) { 62 | unlink($path); 63 | } 64 | } 65 | } 66 | 67 | $path = $folder . '/' . $name . '.csv'; 68 | file_put_contents($path, $contents); 69 | 70 | return compact('path'); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /php/Job/Row/Create.php: -------------------------------------------------------------------------------- 1 | getSpace(); 16 | 17 | $values = get_object_vars($this->values); 18 | 19 | foreach ($values as $k => $v) { 20 | $type = $space->getFieldFormat($k)['type']; 21 | if ($type === 'uuid') { 22 | $v = new Uuid($v); 23 | } elseif ($type == 'map') { 24 | if ($v !== null) { 25 | if (is_string($v)) { 26 | $v = json_decode($v); 27 | } 28 | if (!is_array($v) && !is_object($v)) { 29 | throw new Exception("Invalid type for '$k' ($type): $values[$k]"); 30 | } 31 | $v = $this->toArray($v); 32 | if (!count($v)) { 33 | $v = null; 34 | } 35 | } 36 | } elseif (is_object($v)) { 37 | $v = $this->toArray($v); 38 | } 39 | $values[$k] = $v; 40 | } 41 | return ['row' => $space->create($values)]; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /php/Job/Row/Job.php: -------------------------------------------------------------------------------- 1 | $v) { 18 | if (is_array($v) || is_object($v)) { 19 | $data[$k] = $this->toArray($v); 20 | } 21 | } 22 | return $data; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /php/Job/Row/Remove.php: -------------------------------------------------------------------------------- 1 | getSpace(); 16 | $params = get_object_vars($this->id); 17 | if (!count($params)) { 18 | throw new Exception("Invalid params"); 19 | } 20 | $space->delete($space->findOrFail($params)); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /php/Job/Row/Update.php: -------------------------------------------------------------------------------- 1 | getSpace(); 18 | 19 | $format = $this->getFormat(); 20 | foreach ($this->getMapper()->find('_vindex', ['id' => $space->getId()])[0]['parts'] as $part) { 21 | $fieldName = $format[array_key_exists(0, $part) ? $part[0] : $part['field']]['name']; 22 | $pk[$fieldName] = $this->values->$fieldName; 23 | } 24 | $row = $space->findOrFail($pk); 25 | $changes = []; 26 | foreach ($this->values as $k => $v) { 27 | $type = $space->getFieldFormat($k)['type']; 28 | if ($type === 'uuid') { 29 | $v = new Uuid($v); 30 | } elseif ($type == '*') { 31 | if (is_object($v)) { 32 | $v = $this->toArray($v); 33 | } 34 | } elseif ($type == 'map') { 35 | if ($v !== null) { 36 | if (is_string($v)) { 37 | $v = json_decode($v); 38 | } 39 | if (!is_array($v) && !is_object($v)) { 40 | $extra = ''; 41 | if (is_string($this->values->$k)) { 42 | $extra .= ': ' . $this->values->$k; 43 | } 44 | throw new Exception("Invalid type for '$k' ($type)$extra"); 45 | } 46 | $v = $this->toArray($v); 47 | if (!count($v)) { 48 | $v = null; 49 | } 50 | } 51 | } elseif ($type == 'unsigned') { 52 | if (is_string($v)) { 53 | $v = intval($v); 54 | } 55 | } elseif ($type == 'number') { 56 | if (is_string($v)) { 57 | $v = floatval($v); 58 | } 59 | } 60 | $changes[$k] = $v; 61 | } 62 | $space->update($row, $changes); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /php/Job/Space/AddProperty.php: -------------------------------------------------------------------------------- 1 | getSpace(); 16 | if (in_array($this->name, $space->getFields())) { 17 | throw new Exception("Property $this->name already exists"); 18 | } 19 | 20 | $space->addProperty($this->name, $this->type, [ 21 | 'is_nullable' => $this->is_nullable, 22 | ]); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /php/Job/Space/Create.php: -------------------------------------------------------------------------------- 1 | getMapper()->hasSpace($this->space)) { 12 | throw new Exception("Space $this->space already exists"); 13 | } 14 | 15 | $this->getMapper()->createSpace($this->space); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /php/Job/Space/CreateIndex.php: -------------------------------------------------------------------------------- 1 | getSpace(); 18 | 19 | if (is_array($this->parts)) { 20 | $spaceId = $space->getId(); 21 | $options = [ 22 | 'type' => $this->type, 23 | 'unique' => $this->unique, 24 | 'parts' => $this->parts, 25 | ]; 26 | $space->mapper->client->call("box.space[$spaceId]:create_index", $this->name, [$options]); 27 | } elseif (is_array($this->fields)) { 28 | $space->addIndex($this->fields, [ 29 | 'name' => $this->name, 30 | 'unique' => $this->unique, 31 | 'type' => $this->type, 32 | ]); 33 | } else { 34 | throw new Exception("Invalid index configuration: " . json_encode([ 35 | 'name' => $this->name, 36 | 'type' => $this->type, 37 | ])); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /php/Job/Space/Drop.php: -------------------------------------------------------------------------------- 1 | getSpace(); 12 | if ($space->getId() < 512) { 13 | throw new Exception('Disabled for system spaces'); 14 | } 15 | 16 | $this->getClient()->call('box.space.' . $space->getName() . ':drop'); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /php/Job/Space/Info.php: -------------------------------------------------------------------------------- 1 | getSpace(); 12 | $format = $this->getFormat(); 13 | $indexes = $this->getMapper()->find('_vindex', [ 14 | 'id' => $space->getId(), 15 | ]); 16 | $fake = !count($format); 17 | 18 | if ($fake) { 19 | $format = []; 20 | $count = $this->getClient()->evaluate("return box.space['" . $space->getName() . "'].field_count")[0]; 21 | $count = $count ?: 20; // default max columns 22 | foreach (range(1, $count) as $value) { 23 | $format[] = [ 24 | 'name' => "" . $value, 25 | 'type' => 'str', 26 | ]; 27 | } 28 | } 29 | 30 | foreach ($indexes as $i => $index) { 31 | try { 32 | $indexes[$i]['id'] = $index['iid']; 33 | $indexes[$i]['size'] = $this->getClient()->call( 34 | "box.space." . $space->getName() . ".index." . $index['name'] . ":bsize" 35 | )[0]; 36 | } catch (Exception) { 37 | // no bsize 38 | break; 39 | } 40 | } 41 | 42 | return compact('format', 'indexes', 'fake'); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /php/Job/Space/Job.php: -------------------------------------------------------------------------------- 1 | spaceInstance)) { 19 | return $this->spaceInstance; 20 | } 21 | 22 | if (!$this->space) { 23 | throw new Exception('space name is not defined'); 24 | } 25 | 26 | return $this->spaceInstance = $this->getMapper()->getSpace($this->space); 27 | } 28 | 29 | public function getFormat() 30 | { 31 | $format = []; 32 | foreach ($this->spaceInstance->getFields() as $field) { 33 | $format[] = $this->spaceInstance->getFieldFormat($field); 34 | } 35 | return $format; 36 | } 37 | 38 | public function trimTail($arr): array 39 | { 40 | $trimArr = []; 41 | foreach ($arr as $value) { 42 | if ($value === null) { 43 | break; 44 | } 45 | $trimArr[] = $value; 46 | } 47 | return $trimArr; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /php/Job/Space/RemoveIndex.php: -------------------------------------------------------------------------------- 1 | getSpace(); 14 | $indexExist = false; 15 | foreach ($space->mapper->find('_vindex', ['id' => $space->getId()]) as $index) { 16 | if ($index['name'] == $this->name) { 17 | $space->mapper->client->call("box.space." . $space->getName() . ".index.$this->name:drop"); 18 | $indexExist = true; 19 | } 20 | } 21 | if (!$indexExist) { 22 | throw new Exception("Index $this->name not found"); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /php/Job/Space/RemoveProperty.php: -------------------------------------------------------------------------------- 1 | getSpace(); 14 | $fields = $space->getFields(); 15 | $partsNumbers = []; 16 | foreach ($space->mapper->find('_vindex', ['id' => $space->getId()]) as $index) { 17 | foreach ($index['parts'] as $part) { 18 | $partsNumbers[] = (array_key_exists('field', $part) ? $part['field'] : $part[0]); 19 | }; 20 | } 21 | 22 | if (!in_array($this->name, $fields)) { 23 | throw new Exception("Property $this->name does not exist"); 24 | } 25 | 26 | if (!count($fields)) { 27 | $space->getFieldFormat($this->name); 28 | } 29 | 30 | if (array_reverse($fields)[0] !== $this->name) { 31 | throw new Exception("Remove only last property"); 32 | } elseif (in_array(array_search($this->name, $fields), $partsNumbers)) { 33 | throw new Exception("This property is the part of index. Remove related index first."); 34 | } else { 35 | $format = $this->getFormat(); 36 | array_pop($format); 37 | $space->mapper->client->call("box.space." . $space->getName() . ":format", $format); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /php/Job/Space/Select.php: -------------------------------------------------------------------------------- 1 | trimTail($this->key); 22 | 23 | $data = null; 24 | $total = null; 25 | $next = false; 26 | 27 | try { 28 | $criteria = Criteria::index($this->index) 29 | ->andLimit($this->limit) 30 | ->andOffset($this->offset) 31 | ->andIterator($this->iterator); 32 | 33 | if (count($key)) { 34 | $criteria = $criteria->andKey($key); 35 | } 36 | 37 | $data = $this->getMapper()->client->getSpace($this->space) 38 | ->select($criteria); 39 | 40 | foreach ($data as $x => $tuple) { 41 | foreach ($tuple as $y => $value) { 42 | if ($value instanceof Decimal) { 43 | $value = $value->toString(); 44 | } elseif ($value instanceof Uuid) { 45 | $value = $value->toRfc4122(); 46 | } 47 | $data[$x][$y] = $value; 48 | } 49 | } 50 | 51 | try { 52 | [$space] = $this->getMapper()->find( 53 | '_vspace', 54 | ['name' => $this->space] 55 | ); 56 | if (!in_array($this->iterator, [0, 2])) { 57 | throw new Exception( 58 | "No total rows for non-equals iterator type" 59 | ); 60 | } 61 | if ($space['engine'] == 'vinyl') { 62 | if (getenv('TARANTOOL_ENABLE_VINYL_PAGE_COUNT') !== false) { 63 | throw new Exception("No total rows for vinyl spaces"); 64 | } 65 | } 66 | 67 | $index = $this->getMapper()->findOrFail( 68 | '_vindex', [ 69 | 'id' => $space['id'], 70 | 'iid' => $this->index 71 | ] 72 | ); 73 | 74 | $indexName = $index['name']; 75 | 76 | [$total] = $this->getMapper()->client->call( 77 | "box.space.$this->space.index.$indexName:count", 78 | $key 79 | ); 80 | } catch (Exception) { 81 | $criteria = $criteria->andLimit($this->limit + 1); 82 | $extra = $this->getMapper()->client->getSpace($this->space) 83 | ->select($criteria); 84 | // next page flag 85 | $next = count($extra) > count($data); 86 | } 87 | } catch (Exception $e) { 88 | if (!$data) { 89 | throw $e; 90 | } 91 | } 92 | 93 | if (!json_encode($data)) { 94 | foreach ($data as $i => &$tuple) { 95 | array_walk_recursive( 96 | $tuple, function (&$v) { 97 | if (is_string($v) && !json_encode($v)) { 98 | $v = '!!binary ' . base64_encode($v); 99 | } 100 | } 101 | ); 102 | } 103 | } 104 | 105 | return compact('data', 'total', 'next'); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /php/Job/Space/Truncate.php: -------------------------------------------------------------------------------- 1 | getSpace(); 14 | 15 | if ($space->getId() < 512) { 16 | throw new Exception('Disabled for system spaces'); 17 | } 18 | 19 | if (count($this->key) == 0) { 20 | $this->getClient()->call('box.space.' . $space->getName() . ':truncate'); 21 | } else { 22 | $this->getClient()->evaluate( 23 | 'local space, index, key, iterator = ... 24 | box.begin() 25 | box.space[space].index[index]:pairs(key, {iterator=iterator}) 26 | :each(function(tuple) 27 | local pk = {} 28 | for _, part in pairs(box.space[space].index[0].parts) do 29 | table.insert(pk, tuple[part.fieldno]) 30 | end 31 | box.space[space]:delete(pk) 32 | end) 33 | box.commit()', 34 | $space->getName(), 35 | $this->index, 36 | $this->trimTail($this->key), 37 | $this->iterator 38 | ); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /public/.htaccess: -------------------------------------------------------------------------------- 1 | RewriteEngine on 2 | 3 | RewriteCond %{REQUEST_FILENAME} !-d 4 | RewriteCond %{REQUEST_FILENAME} !-f 5 | RewriteRule ^ server.php [L] 6 | -------------------------------------------------------------------------------- /public/admin/39156475-8b873e18-4756-11e8-89d0-6ffca592f664.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basis-company/tarantool-admin/491435ffbcfbea21e1d4f8877de487debe31d0a9/public/admin/39156475-8b873e18-4756-11e8-89d0-6ffca592f664.png -------------------------------------------------------------------------------- /public/admin/index.php: -------------------------------------------------------------------------------- 1 | { 23 | this.down('[name=info]').setSource(result.info); 24 | this.down('[name=slab]').setSource(result.slab); 25 | this.down('[name=stat]').store.loadData(Ext.Object.getKeys(result.stat).map(k => { 26 | return { 27 | action: k, 28 | rps: result.stat[k].rps, 29 | total: result.stat[k].total, 30 | }; 31 | })); 32 | }) 33 | .catch(() => this.close()); 34 | }, 35 | }, 36 | 37 | items: [ { 38 | width: 250, 39 | tbar: { 40 | height: 36, 41 | items: [ { 42 | xtype: 'label', 43 | text: 'Instance information', 44 | } ], 45 | }, 46 | name: 'info', 47 | xtype: 'propertygrid', 48 | nameColumnWidth: 80, 49 | listeners: { 50 | beforeedit: function() { 51 | return false; 52 | }, 53 | }, 54 | source: {}, 55 | customRenderers: { 56 | quota_used: Ext.util.Format.fileSize, 57 | }, 58 | }, { 59 | tbar: { 60 | height: 36, 61 | items: [ { 62 | xtype: 'label', 63 | text: 'Query counters', 64 | } ], 65 | }, 66 | width: 300, 67 | name: 'stat', 68 | readonly: true, 69 | xtype: 'grid', 70 | store: { 71 | fields: [ 'action', 'rps', 'total' ], 72 | sorters: [ { property: 'action', direction: 'ASC' } ], 73 | }, 74 | columns: [ { 75 | header: 'Action', 76 | dataIndex: 'action', 77 | renderer(v) { 78 | return (v || '').toLowerCase(); 79 | }, 80 | }, { 81 | header: 'Rps', 82 | dataIndex: 'rps', 83 | align: 'center', 84 | renderer(v) { 85 | return v || '-'; 86 | }, 87 | }, { 88 | header: 'Total', 89 | align: 'right', 90 | dataIndex: 'total', 91 | renderer(v) { 92 | return v || '-'; 93 | }, 94 | } ], 95 | }, { 96 | width: 250, 97 | tbar: { 98 | height: 36, 99 | items: [ { 100 | xtype: 'label', 101 | text: 'Memory usage', 102 | } ], 103 | }, 104 | name: 'slab', 105 | xtype: 'propertygrid', 106 | nameColumnWidth: 150, 107 | listeners: { 108 | beforeedit: function() { 109 | return false; 110 | }, 111 | }, 112 | source: {}, 113 | customRenderers: { 114 | arena_size: Ext.util.Format.fileSize, 115 | arena_used: Ext.util.Format.fileSize, 116 | items_size: Ext.util.Format.fileSize, 117 | items_used: Ext.util.Format.fileSize, 118 | quota_size: Ext.util.Format.fileSize, 119 | quota_used: Ext.util.Format.fileSize, 120 | }, 121 | } ], 122 | }); 123 | -------------------------------------------------------------------------------- /public/admin/js/Database/Query.js: -------------------------------------------------------------------------------- 1 | Ext.define('Admin.Database.Query', { 2 | 3 | extend: 'Ext.panel.Panel', 4 | title: 'Query', 5 | iconCls: 'fa fa-code', 6 | border: false, 7 | 8 | layout: { 9 | type: 'vbox', 10 | align: 'stretch', 11 | }, 12 | 13 | listeners: { 14 | single: true, 15 | afterlayout() { 16 | this.down('textarea').focus(); 17 | }, 18 | }, 19 | 20 | items: [ { 21 | tbar: { 22 | height: 36, 23 | items: [ { 24 | xtype: 'label', 25 | text: 'Type your query below', 26 | } ], 27 | }, 28 | style: { 29 | marginLeft: '8px', 30 | marginRight: '8px', 31 | }, 32 | layout: 'fit', 33 | items: [ { 34 | xtype: 'textarea', 35 | value: 'return box.space._space:select()', 36 | cls: 'query-textarea', 37 | grow: true, 38 | flex: 1, 39 | maxHeight: 300, 40 | listeners: { 41 | specialkey(f, e) { 42 | if (e.keyCode == 13 && e.ctrlKey) { 43 | f.up('database-query') 44 | .down('[text=Execute]') 45 | .handler(); 46 | } 47 | }, 48 | }, 49 | } ], 50 | }, { 51 | bodyPadding: 10, 52 | flex: 1, 53 | layout: 'fit', 54 | name: 'result', 55 | showResult(script, result) { 56 | var returns = (script.split('return ')[1] || '').split(',').map(v => v.trim()); 57 | 58 | this.removeAll(); 59 | var stats = 'Your query takes ' + Ext.util.Format.number(result.timing, '0.0') + ' ms'; 60 | 61 | this.up('[title=Query]') 62 | .down('[name=execution]') 63 | .setText(stats); 64 | 65 | this.add(Ext.create('Ext.tab.Panel', { 66 | layout: 'fit', 67 | items: result.result.map((element, i) => { 68 | var item = { 69 | title: returns.length == result.result.length ? returns[i] : i+1, 70 | layout: 'fit', 71 | }; 72 | 73 | if (Ext.isArray(element) && Ext.isArray(element[0]) && !Ext.isArray(element[0][0])) { 74 | item.xtype = 'grid'; 75 | item.columns = []; 76 | var tupleLength = Ext.Array.max(element.map(row => row.length)); 77 | 78 | for (var j=1; j <= tupleLength; j++) { 79 | item.columns.push({ 80 | dataIndex: 'f' + j, 81 | header: j, 82 | autoSize: true, 83 | renderer(v) { 84 | if (Ext.isObject(v)) { 85 | return Ext.JSON.encode(v); 86 | } 87 | 88 | if (Ext.isArray(v) && v.length && (Ext.isArray(v[0]) || Ext.isObject(v[0]))) { 89 | return Ext.JSON.encode(v); 90 | } 91 | 92 | return v; 93 | }, 94 | }); 95 | } 96 | 97 | item.store = { 98 | fields: item.columns.map(c => c.dataIndex), 99 | data: element, 100 | }; 101 | } 102 | else { 103 | var getChildren = function(object) { 104 | return Ext.Object.getKeys(object) 105 | .sort() 106 | .map(key => { 107 | var node = { 108 | key: key, 109 | value: object[key], 110 | }; 111 | 112 | if (Ext.isObject(node.value)) { 113 | node.children = getChildren(node.value); 114 | node.value = '...'; 115 | } 116 | else { 117 | node.leaf = true; 118 | } 119 | 120 | return node; 121 | }); 122 | }; 123 | 124 | Ext.apply(item, { 125 | xtype: 'treepanel', 126 | rootVisible: false, 127 | root: { 128 | text: 'root', 129 | expanded: true, 130 | children: getChildren(element), 131 | }, 132 | columns: [ { 133 | text: 'Key', 134 | dataIndex: 'key', 135 | width: 200, 136 | xtype: 'treecolumn', 137 | }, { 138 | text: 'Value', 139 | flex: 1, 140 | dataIndex: 'value', 141 | } ], 142 | }); 143 | } 144 | 145 | return item; 146 | }), 147 | })); 148 | }, 149 | tbar: [ { 150 | text: 'Execute', 151 | iconCls: 'fa fa-play', 152 | handler() { 153 | var script = this.up('database-query') 154 | .down('textarea') 155 | .getValue(); 156 | 157 | dispatch('database.execute', Ext.apply({ code: script }, this.up('database-tab').params)) 158 | .then(result => { 159 | this.up('database-query') 160 | .down('[name=result]') 161 | .showResult(script, result); 162 | }); 163 | }, 164 | }, '->', { 165 | xtype: 'label', 166 | name: 'execution', 167 | } ], 168 | } ], 169 | }); 170 | -------------------------------------------------------------------------------- /public/admin/js/Database/Spaces.js: -------------------------------------------------------------------------------- 1 | Ext.define('Admin.Database.Spaces', { 2 | 3 | extend: 'Ext.grid.Panel', 4 | 5 | name: 'spaces', 6 | title: 'Spaces', 7 | iconCls: 'fa fa-bars', 8 | border: false, 9 | 10 | isUserSpace(record) { 11 | return record.get('id') >= 512; 12 | }, 13 | 14 | refreshSpaces: function() { 15 | dispatch('database.spaces', this.up('database-tab').params) 16 | .then(result => { 17 | this.store.loadData(result.spaces); 18 | }); 19 | }, 20 | 21 | createSpace() { 22 | var win = Ext.create('Ext.window.Window', { 23 | modal: true, 24 | title: 'New space', 25 | items: [ { 26 | xtype: 'form', 27 | bodyPadding: 10, 28 | items: [ { 29 | fieldLabel: 'Name', 30 | xtype: 'textfield', 31 | allowBlank: false, 32 | name: 'name', 33 | } ], 34 | bbar: [ '->', { 35 | formBind: true, 36 | text: 'Create', 37 | handler: () => { 38 | var space = win.down('form').getValues().name; 39 | 40 | dispatch('space.create', this.spaceParams(space)) 41 | .then(() => { 42 | win.close(); 43 | this.refreshSpaces(); 44 | }); 45 | }, 46 | } ], 47 | } ], 48 | }); 49 | 50 | win.show(); 51 | win.down('textfield').focus(); 52 | }, 53 | 54 | spaceParams(space) { 55 | return Ext.apply({ 56 | space: space, 57 | }, this.up('database-tab').params); 58 | }, 59 | 60 | showSpace(space) { 61 | var exists = false; 62 | 63 | this.up('database-tab').items.each(item => { 64 | if (item.params && item.params.space == space) { 65 | this.up('database-tab').setActiveItem(item); 66 | exists = true; 67 | } 68 | }); 69 | 70 | if (!exists) { 71 | var view = Ext.create('Admin.Space.Tab', { 72 | params: this.spaceParams(space), 73 | }); 74 | 75 | this.up('database-tab').add(view); 76 | this.up('database-tab').setActiveItem(view); 77 | } 78 | }, 79 | 80 | keyEmptyCheck(key) { 81 | return !key || key.every(v => v == null); 82 | }, 83 | 84 | keyValidCheck(key) { 85 | var isValid = true; 86 | 87 | if (this.keyEmptyCheck(key)) { 88 | isValid = false; 89 | } 90 | else { 91 | for (let i=0; i' + 107 | 'This operation can not be undone'; 108 | 109 | if (searchdata && searchdata.index >= 0) { 110 | if (!this.keyValidCheck(searchdata.key)) { 111 | Ext.Msg.alert('Warning!', 'Not valid key. Please, fill in all fields starting from the first.'); 112 | return; 113 | } 114 | 115 | Ext.apply(params, searchdata); 116 | delete params.indexObj; 117 | message = 'Are you sure to delete tuples by index ' + searchdata.indexObj.name + 118 | ' and key ' + searchdata.key + ' from space ' + space + '?
' + 119 | 'This operation can not be undone'; 120 | } 121 | 122 | Ext.MessageBox.confirm({ 123 | title: 'Danger!', 124 | icon: Ext.MessageBox.WARNING, 125 | message: message, 126 | buttons: Ext.MessageBox.YESNO, 127 | callback: (answer) => { 128 | if (answer == 'yes') { 129 | dispatch('space.truncate', params) 130 | .then(() => { 131 | this.refreshSpaces(); 132 | this.up('database-tab').items.each(item => { 133 | if (item.params && item.params.space == space) { 134 | item.items.each(item => { 135 | if (item.xtype == 'space-collection') { 136 | item.store.load(); 137 | } 138 | }); 139 | } 140 | }); 141 | }); 142 | } 143 | }, 144 | }); 145 | }, 146 | 147 | dropSpace(space) { 148 | Ext.MessageBox.confirm( 149 | 'Danger!', 150 | 'Are you sure to drop space ' + space + '?
This operation can not be undone', 151 | answer => { 152 | if (answer == 'yes') { 153 | dispatch('space.drop', this.spaceParams(space)) 154 | .then(() => { 155 | this.refreshSpaces(); 156 | this.up('database-tab').items.each(item => { 157 | if (item.params && item.params.space == space) { 158 | this.up('database-tab').remove(item); 159 | } 160 | }); 161 | }); 162 | } 163 | } 164 | ); 165 | }, 166 | 167 | listeners: { 168 | render: function() { 169 | if (window.configuration.readOnly) { 170 | this.down('[text=Create]').hide(); 171 | this.down('[text=Truncate]').hide(); 172 | this.down('[text=Drop]').hide(); 173 | } 174 | 175 | this.store.addFilter((record) => { 176 | if (this.down('[name=system-spaces]').value) { 177 | return true; 178 | } 179 | 180 | return this.isUserSpace(record); 181 | }); 182 | }, 183 | activate: function() { 184 | if (!this.store.getCount()) { 185 | this.refreshSpaces(); 186 | } 187 | }, 188 | itemdblclick(view, record) { 189 | view.up('database-spaces').showSpace(record.get('name')); 190 | }, 191 | selectionchange(sm, sel) { 192 | this.down('[name=open-button]').setDisabled(!sel.length); 193 | this.down('[name=drop-button]').setDisabled(!sel.length || !this.isUserSpace(sel[0])); 194 | this.down('[name=truncate-button]').setDisabled(!sel.length || !this.isUserSpace(sel[0])); 195 | }, 196 | }, 197 | 198 | store: { 199 | fields: [ { 200 | id: 'id', 201 | type: 'integer', 202 | }, 'name', 'engine', 'count', { 203 | name: 'owner', 204 | type: 'integer', 205 | } ], 206 | sorters: [ { property: 'name', direction: 'ASC' } ], 207 | }, 208 | 209 | tbar: [ { 210 | xtype: 'label', 211 | text: 'Spaces', 212 | }, { 213 | xtype: 'filter-field', 214 | }, { 215 | text: 'Create', 216 | iconCls: 'fa fa-plus', 217 | handler() { 218 | this.up('database-spaces').createSpace(); 219 | }, 220 | }, { 221 | text: 'Open', 222 | name: 'open-button', 223 | iconCls: 'fa fa-table', 224 | disabled: true, 225 | handler() { 226 | this.up('database-spaces').showSpace( 227 | this.up('grid') 228 | .getSelectionModel() 229 | .getSelection()[0] 230 | .get('name') 231 | ); 232 | }, 233 | }, { 234 | text: 'Truncate', 235 | name: 'truncate-button', 236 | iconCls: 'fa fa-trash', 237 | disabled: true, 238 | handler() { 239 | this.up('database-spaces').truncateSpace( 240 | this.up('grid') 241 | .getSelectionModel() 242 | .getSelection()[0] 243 | .get('name') 244 | ); 245 | }, 246 | }, { 247 | text: 'Drop', 248 | name: 'drop-button', 249 | iconCls: 'fa fa-ban', 250 | disabled: true, 251 | handler() { 252 | this.up('database-spaces').dropSpace( 253 | this.up('grid') 254 | .getSelectionModel() 255 | .getSelection()[0] 256 | .get('name') 257 | ); 258 | }, 259 | }, '->', { 260 | text: 'Show system', 261 | iconCls: 'far fa-circle', 262 | name: 'system-spaces', 263 | value: false, 264 | handler() { 265 | this.setIconCls(this.value ? 'far fa-circle' : 'far fa-check-circle'); 266 | this.value = this.iconCls == 'far fa-check-circle'; 267 | this.up('database-spaces').refreshSpaces(); 268 | }, 269 | }, { 270 | text: 'Refresh', 271 | iconCls: 'fa fa-sync', 272 | handler() { 273 | this.up('database-spaces').refreshSpaces(); 274 | }, 275 | } ], 276 | columns: [ { 277 | header: 'Id', 278 | dataIndex: 'id', 279 | align: 'center', 280 | width: 60, 281 | }, { 282 | header: 'Name', 283 | dataIndex: 'name', 284 | width: 200, 285 | }, { 286 | align: 'center', 287 | header: 'Engine', 288 | dataIndex: 'engine', 289 | }, { 290 | header: 'Count', 291 | align: 'right', 292 | dataIndex: 'count', 293 | renderer: v => v == null ? '-' : v, 294 | }, { 295 | header: 'Size', 296 | align: 'right', 297 | dataIndex: 'bsize', 298 | renderer: v => v == null ? '-' : Ext.util.Format.fileSize(v), 299 | } ], 300 | }); 301 | -------------------------------------------------------------------------------- /public/admin/js/Database/Tab.js: -------------------------------------------------------------------------------- 1 | Ext.define('Admin.Database.Tab', { 2 | 3 | extend: 'Ext.tab.Panel', 4 | title: 'Database', 5 | closable: true, 6 | border: false, 7 | 8 | iconCls: 'fa fa-database', 9 | 10 | requires: [ 11 | 'Admin.Database.Info', 12 | 'Admin.Database.Query', 13 | 'Admin.Database.Spaces', 14 | ], 15 | 16 | listeners: { 17 | tabchange(tabs, tab) { 18 | var tabIndex = tabs.items.indexOf(tab); 19 | 20 | if (tab.xtypesChain.indexOf('space-tab') === -1) { 21 | localStorage.setItem('database-default-item', tabIndex); 22 | } 23 | }, 24 | }, 25 | 26 | initComponent() { 27 | var params = this.params; 28 | 29 | this.title = ''; 30 | 31 | if (params.username != 'guest') { 32 | this.title += params.username + ' @ '; 33 | } 34 | 35 | if (params.socket) { 36 | this.title += params.socket; 37 | } 38 | else { 39 | this.title += params.hostname; 40 | 41 | if (params.port != 3301) { 42 | this.title += ' : ' + params.port; 43 | } 44 | } 45 | 46 | this.activeTab = +localStorage.getItem('database-default-item') || 0; 47 | 48 | if (this.activeTab > this.items.length) { 49 | this.activeTab = 0; 50 | } 51 | 52 | if (this.activeTab == 1 && window.Admin.Database.Tab.prototype.items[1].hidden) { 53 | this.activeTab = 0; 54 | } 55 | 56 | this.callParent(arguments); 57 | }, 58 | 59 | items: [ { 60 | xtype: 'database-info', 61 | }, { 62 | xtype: 'database-query', 63 | }, { 64 | xtype: 'database-spaces', 65 | } ], 66 | }); 67 | -------------------------------------------------------------------------------- /public/admin/js/Home/Connections.js: -------------------------------------------------------------------------------- 1 | Ext.define('Admin.Home.Connections', { 2 | 3 | extend: 'Ext.grid.Panel', 4 | 5 | flex: 1, 6 | hidden: true, 7 | 8 | listeners: { 9 | itemdblclick(view, record) { 10 | view.up('home-tab').showDatabase(record.data); 11 | }, 12 | selectionchange(sm, sel) { 13 | this.down('[name=connect-button]').setDisabled(!sel.length); 14 | this.down('[name=remove-button]').setDisabled(!sel.length); 15 | }, 16 | }, 17 | 18 | store: { 19 | fields: [ 'hostname', 'port', 'username', 'password' ], 20 | sorters: [ 21 | { property: 'hostname', direction: 'ASC' }, 22 | { property: 'port', direction: 'ASC' }, 23 | { property: 'username', direction: 'ASC' }, 24 | ], 25 | }, 26 | 27 | tbar: [ { 28 | xtype: 'label', 29 | text: 'Connection list', 30 | }, { 31 | xtype: 'filter-field', 32 | }, { 33 | text: 'Connect', 34 | name: 'connect-button', 35 | iconCls: 'fa fa-link', 36 | disabled: true, 37 | handler() { 38 | var connection = this.up('grid') 39 | .getSelectionModel() 40 | .getSelection()[0] 41 | .data; 42 | 43 | this.up('home-tab').showDatabase(connection); 44 | }, 45 | }, { 46 | text: 'Remove', 47 | name: 'remove-button', 48 | iconCls: 'fa fa-trash', 49 | disabled: true, 50 | hidden: true, 51 | handler() { 52 | Ext.MessageBox.confirm('Confirmation', 'Are you sure want to remove selected connection?
This operation has no rollback!', (btn) => { 53 | if (btn == 'yes') { 54 | var connection = this.up('grid') 55 | .getSelectionModel() 56 | .getSelection()[0] 57 | .data; 58 | 59 | this.up('home-tab').removeConnection(connection); 60 | } 61 | }); 62 | }, 63 | }, { 64 | text: 'Remove all', 65 | name: 'remove-all', 66 | iconCls: 'fa fa-ban', 67 | hidden: true, 68 | handler() { 69 | Ext.MessageBox.confirm('Confirmation', 'Are you sure want to remove all connections?
This operation has no rollback!', (btn) => { 70 | if (btn == 'yes') { 71 | var connection = this.up('grid') 72 | .getSelectionModel() 73 | .getSelection()[0] 74 | .data; 75 | 76 | this.up('home-tab').removeConnection(connection); 77 | } 78 | }); 79 | this.up('home-tab').clearConnections(); 80 | }, 81 | } ], 82 | columns: [ { 83 | dataIndex: 'hostname', 84 | header: 'Hostname', 85 | align: 'center', 86 | width: 150, 87 | renderer(v, e, r) { 88 | return v || r.get('socket'); 89 | }, 90 | }, { 91 | header: 'Port', 92 | dataIndex: 'port', 93 | align: 'center', 94 | renderer(v, el) { 95 | if (v == 3301) { 96 | el.style = 'color: #999'; 97 | } 98 | 99 | return v; 100 | }, 101 | }, { 102 | header: 'Username', 103 | dataIndex: 'username', 104 | align: 'center', 105 | renderer(v, el) { 106 | if (v == 'guest') { 107 | el.style = 'color: #999'; 108 | } 109 | 110 | return v; 111 | }, 112 | } ], 113 | }); 114 | -------------------------------------------------------------------------------- /public/admin/js/Home/New.js: -------------------------------------------------------------------------------- 1 | Ext.define('Admin.Home.New', { 2 | 3 | extend: 'Ext.form.Panel', 4 | 5 | height: 200, 6 | border: false, 7 | hidden: true, 8 | style: { 9 | paddingRight: '15px', 10 | }, 11 | 12 | defaults: { 13 | xtype: 'textfield', 14 | labelWidth: 80, 15 | width: 250, 16 | enableKeyEvents: true, 17 | style: { 18 | paddingLeft: '9px', 19 | }, 20 | listeners: { 21 | specialkey(field, e) { 22 | if (e.getKey() == e.ENTER) { 23 | this.up('home-tab').createConnection(); 24 | } 25 | }, 26 | }, 27 | }, 28 | 29 | items: [ { 30 | fieldLabel: 'Hostname', 31 | allowBlank: false, 32 | name: 'hostname', 33 | }, { 34 | fieldLabel: 'Port', 35 | name: 'port', 36 | xtype: 'numberfield', 37 | minValue: 0, 38 | emptyText: 3301, 39 | }, { 40 | fieldLabel: 'Username', 41 | name: 'username', 42 | emptyText: 'guest', 43 | }, { 44 | fieldLabel: 'Password', 45 | name: 'password', 46 | inputType: 'password', 47 | }, { 48 | xtype: 'checkbox', 49 | boxLabel: 'remember connection', 50 | checked: true, 51 | name: 'remember', 52 | }, { 53 | xtype: 'button', 54 | style: { 55 | marginLeft: '10px', 56 | }, 57 | text: 'Connect', 58 | name: 'connect-button', 59 | iconCls: 'fa fa-link', 60 | formBind: true, 61 | handler() { 62 | this.up('home-tab').createConnection(); 63 | }, 64 | } ], 65 | tbar: { 66 | height: 36, 67 | items: [ { 68 | xtype: 'label', 69 | text: 'New connection', 70 | } ], 71 | }, 72 | }); 73 | -------------------------------------------------------------------------------- /public/admin/js/Home/Tab.js: -------------------------------------------------------------------------------- 1 | Ext.define('Admin.Home.Tab', { 2 | 3 | extend: 'Ext.panel.Panel', 4 | title: 'Home', 5 | iconCls: 'fa fa-home', 6 | border: false, 7 | layout: { 8 | type: 'hbox', 9 | align: 'stretch', 10 | }, 11 | 12 | requires: [ 13 | 'Admin.Home.New', 14 | 'Admin.Home.Connections', 15 | 'Admin.Database.Tab', 16 | ], 17 | 18 | listeners: { 19 | render: function() { 20 | this.refreshConnections() 21 | .then(() => { 22 | var connections = this.down('home-connections'); 23 | var counter = connections.store.getCount(); 24 | 25 | if (counter == 1) { 26 | var connection = connections.store.getAt(0).data; 27 | 28 | setTimeout(() => this.showDatabase(connection), 100); 29 | } 30 | else if (counter > 1) { 31 | this.down('filter-field').focus(); 32 | } 33 | }); 34 | }, 35 | }, 36 | 37 | showDatabase(params) { 38 | params = { 39 | hostname: params.hostname, 40 | socket: params.socket, 41 | port: params.port, 42 | username: params.username, 43 | password: params.password, 44 | }; 45 | var exists = false; 46 | 47 | this.up('tabpanel').items.each(item => { 48 | if (item.params && Ext.JSON.encode(item.params) == Ext.JSON.encode(params)) { 49 | this.up('tabpanel').setActiveItem(item); 50 | exists = true; 51 | } 52 | }); 53 | 54 | if (!exists) { 55 | var view = Ext.create('Admin.Database.Tab', { params: params }); 56 | 57 | this.up('tabpanel').add(view); 58 | this.up('tabpanel').setActiveItem(view); 59 | } 60 | }, 61 | 62 | createConnection() { 63 | var form = this.down('home-new'); 64 | 65 | if (form.isValid()) { 66 | var connection = form.getValues(); 67 | 68 | connection.port = connection.port || 3301; 69 | connection.username = connection.username || 'guest'; 70 | form.reset(); 71 | 72 | if (connection.remember) { 73 | var connections = Ext.JSON.decode(localStorage.getItem('connections')) || []; 74 | 75 | connections.push(this.getConnectionString(connection.hostname, connection.port, connection.username, connection.password)); 76 | localStorage.setItem('connections', Ext.JSON.encode(connections)); 77 | this.refreshConnections(); 78 | } 79 | 80 | this.showDatabase(connection); 81 | } 82 | }, 83 | 84 | refreshConnections() { 85 | var grid = this.down('home-connections'); 86 | 87 | grid.store.loadData([]); 88 | 89 | var connections = Ext.JSON.decode(localStorage.getItem('connections')) || []; 90 | 91 | return dispatch('admin.configuration') 92 | .then(result => { 93 | window.configuration = result; 94 | 95 | if (result.version && result.version.tag) { 96 | let version = Ext.ComponentQuery.query('[name=version]')[0]; 97 | var legacy = result.latest && result.latest != result.version.tag; 98 | 99 | version.setText('version ' + result.version.tag); 100 | 101 | if (legacy) { 102 | version.addCls('version-upgrade'); 103 | version.setIconCls('fas fa-bell'); 104 | 105 | Ext.create('Ext.tip.ToolTip', { 106 | target: version, 107 | autoShow: true, 108 | autoHide: true, 109 | html: [ 110 | 'new version available!', 111 | ], 112 | }); 113 | } 114 | } 115 | 116 | this.down('home-new').setHidden(result.connectionsReadOnly); 117 | this.down('home-connections').show(); 118 | grid.down('[name=remove-button]').setHidden(result.connectionsReadOnly); 119 | grid.down('[name=remove-all]').setHidden(result.connectionsReadOnly); 120 | 121 | Ext.require('Admin.Database.Tab', function() { 122 | window.Admin.Database.Tab.prototype.items[1].hidden = !result.query; 123 | }); 124 | 125 | if (Ext.isArray(result.connections) && result.connections[0].length) { 126 | var map = {}; 127 | 128 | connections.concat(result.connections).forEach(string => { 129 | let connection = this.parseConnectionString(string); 130 | let key = connection.username + '@' + connection.hostname + ':' + connection.port; 131 | 132 | map[key] = string; 133 | }); 134 | connections = Ext.Object.getValues(map); 135 | } 136 | 137 | if (connections.length) { 138 | grid.show(); 139 | grid.store.loadData(connections.map(string => this.parseConnectionString(string))); 140 | } 141 | else { 142 | grid.hide(); 143 | } 144 | }); 145 | }, 146 | 147 | removeConnection(connection) { 148 | var connections = Ext.JSON.decode(localStorage.getItem('connections')) || []; 149 | var dsn = connection.username + '@' + connection.hostname + ':' + connection.port; 150 | 151 | connections 152 | .filter(candidate => { 153 | var connection = this.parseConnectionString(candidate); 154 | 155 | return connection.username + '@' + connection.hostname + ':' + connection.port == dsn; 156 | }) 157 | .forEach(todo => Ext.Array.remove(connections, todo)); 158 | 159 | localStorage.setItem('connections', Ext.JSON.encode(connections)); 160 | 161 | this.refreshConnections(); 162 | }, 163 | 164 | clearConnections() { 165 | localStorage.removeItem('connections'); 166 | this.refreshConnections(); 167 | }, 168 | 169 | getConnectionString(hostname, port, username, password) { 170 | var connection = ''; 171 | 172 | if (!port) { 173 | port = 3301; 174 | } 175 | 176 | if (username && username != 'guest') { 177 | connection += username; 178 | } 179 | 180 | if (password && password !== '') { 181 | if (!connection.length) { 182 | connection = 'guest'; 183 | } 184 | 185 | connection += ':' + password; 186 | } 187 | 188 | connection = connection.length ? connection + '@' + hostname : hostname; 189 | 190 | if (port && port != 3301) { 191 | connection += ':' + port; 192 | } 193 | 194 | return connection; 195 | }, 196 | 197 | parseConnectionString(connection) { 198 | var hostname = null; 199 | var port = 3301; 200 | var username = 'guest'; 201 | var password = ''; 202 | 203 | var hostport = connection; 204 | var userpass = null; 205 | 206 | if (connection.indexOf('unix://') === 0) { 207 | var socket = connection; 208 | 209 | if (connection.indexOf('@') !== -1) { 210 | socket = connection.split('@')[1]; 211 | let auth = connection.split('@')[0].split('unix://')[1]; 212 | 213 | [ username, password ] = auth.split(':'); 214 | } 215 | 216 | return { socket, username, password }; 217 | } 218 | 219 | if (connection.indexOf('@') === -1) { 220 | hostport = connection; 221 | } 222 | else { 223 | [ userpass, hostport ] = connection.split('@'); 224 | } 225 | 226 | if (hostport.indexOf(':') === -1) { 227 | hostname = hostport; 228 | } 229 | else { 230 | [ hostname, port ] = hostport.split(':'); 231 | } 232 | 233 | if (userpass) { 234 | if (userpass.indexOf(':') === -1) { 235 | username = userpass; 236 | } 237 | else { 238 | [ username, password ] = userpass.split(':'); 239 | } 240 | } 241 | 242 | return { hostname, port, username, password }; 243 | }, 244 | 245 | items: [ { 246 | xtype: 'home-new', 247 | }, { 248 | xtype: 'home-connections', 249 | } ], 250 | }); 251 | -------------------------------------------------------------------------------- /public/admin/js/Space/Collection.js: -------------------------------------------------------------------------------- 1 | Ext.define('Admin.Space.Collection', { 2 | 3 | extend: 'Ext.grid.Panel', 4 | 5 | title: 'Data', 6 | 7 | iconCls: 'fa fa-table', 8 | 9 | requires: [ 10 | 'Admin.data.proxy.PagingDispatch', 11 | 'Admin.Space.Indexes', 12 | ], 13 | 14 | selModel: { 15 | type: 'spreadsheet', 16 | columnSelect: true, 17 | listeners: { 18 | selectionchange(grid, sel) { 19 | if (this.view.grid.down('[text=Update]')) { 20 | this.view.grid.down('[text=Update]').setDisabled(!sel.length); 21 | this.view.grid.down('[text=Delete]').setDisabled(!sel.length); 22 | } 23 | }, 24 | }, 25 | }, 26 | 27 | plugins: { 28 | ptype: 'clipboard', 29 | }, 30 | 31 | tbar: [ { 32 | iconCls: 'fa fa-chevron-left', 33 | }, { 34 | xtype: 'label', 35 | name: 'paging-info', 36 | }, { 37 | iconCls: 'fa fa-chevron-right', 38 | } ], 39 | 40 | listeners: { 41 | render() { 42 | if (window.configuration.readOnly) { 43 | this.down('[text=Create]').hide(); 44 | this.down('[text=Update]').hide(); 45 | this.down('[text=Delete]').hide(); 46 | this.down('[name=truncate]').hide(); 47 | } 48 | }, 49 | columnresize(table, column, width) { 50 | if (width != 50) { 51 | var config = table.grid.getWidthConfig(); 52 | 53 | config[column.fullColumnIndex] = width; 54 | localStorage.setItem(table.grid.params.space+'_width', Ext.JSON.encode(config)); 55 | } 56 | }, 57 | }, 58 | 59 | getWidthConfig() { 60 | var config = localStorage.getItem(this.params.space+'_width'); 61 | 62 | if (config) { 63 | config = Ext.JSON.decode(config); 64 | } 65 | 66 | if (!config) { 67 | config = []; 68 | } 69 | 70 | return config; 71 | }, 72 | 73 | autoLoad: true, 74 | 75 | initComponent() { 76 | if (!this.params) { 77 | this.params = this.up('space-tab').params; 78 | } 79 | 80 | this.tbar = Ext.create('Admin.Space.toolbar.Collection', { 81 | params: this.params, 82 | }); 83 | 84 | if (this.params.index !== undefined) { 85 | this.closable = true; 86 | this.iconCls = 'fa fa-search'; 87 | } 88 | 89 | this.callParent(arguments); 90 | 91 | if (!localStorage.getItem('admin-page-size') && this.autoLoad) { 92 | this.on('reconfigure', () => this.store.load()); 93 | } 94 | 95 | this.on('itemdblclick', () => { 96 | this.down('[text=Update]').handler(); 97 | }); 98 | 99 | this.on({ 100 | single: true, 101 | activate: () => { 102 | dispatch('space.info', this.params) 103 | .then(result => { 104 | var fields = []; 105 | 106 | result.format.forEach(p => fields.push(p.name)); 107 | 108 | this.fields = fields; 109 | this.format = result.format; 110 | this.indexes = result.indexes; 111 | 112 | var config = this.getWidthConfig(); 113 | 114 | var store = Ext.create('Ext.data.ArrayStore', { 115 | model: Ext.define(null, { 116 | extend: 'Ext.data.Model', 117 | fields: [ '_' ].concat(fields), 118 | idProperty: '_', 119 | }), 120 | proxy: 'pagingdispatch', 121 | listeners: { 122 | load: () => { 123 | this.down('[name=export]').setDisabled(!this.store.getCount()); 124 | var maxSize = 0; 125 | 126 | if (result.fake) { 127 | this.store.getRange().forEach(r => { 128 | if (Ext.Object.getSize(r.data) > maxSize) { 129 | maxSize = Ext.Object.getSize(r.data); 130 | } 131 | }); 132 | } 133 | 134 | columns.forEach((c, n) => { 135 | if (result.fake) { 136 | if (n >= maxSize-1) { 137 | this.getColumns()[n+1].hide(); 138 | } 139 | else { 140 | this.getColumns()[n+1].show(); 141 | } 142 | } 143 | }); 144 | 145 | if (Ext.Object.getSize(config) === 0) { 146 | columns.forEach((c, n) => { 147 | this.view.autoSizeColumn(n); 148 | }); 149 | } 150 | 151 | this.down('toolbar-collection').updateState(); 152 | }, 153 | }, 154 | }); 155 | 156 | store.proxy.job = 'space.select'; 157 | store.proxy.params = this.params; 158 | 159 | var columns = fields.map((f, i) => { 160 | return { 161 | hidden: result.fake, 162 | dataIndex: f, 163 | header: f, 164 | width: +config[i+1] || 50, 165 | renderer: (v) => { 166 | if (Ext.isObject(v) || (Ext.isArray(v) && v[0])) { 167 | v = Ext.JSON.encode(v); 168 | } 169 | 170 | if (Ext.isString(v) && v.indexOf('<') !== -1) { 171 | v = Ext.String.htmlEncode(v); 172 | } 173 | 174 | return v; 175 | }, 176 | }; 177 | }); 178 | 179 | this.down('toolbar-collection').applyMeta(); 180 | 181 | if (this.params.index !== undefined) { 182 | this.addDocked(Ext.create('Admin.Space.toolbar.Search', { 183 | collection: this, 184 | }), 0); 185 | } 186 | 187 | this.reconfigure(store, columns); 188 | 189 | if (localStorage.getItem('admin-page-size')) { 190 | this.down('[name=pageSize]').setValue(localStorage.getItem('admin-page-size')); 191 | } 192 | }); 193 | }, 194 | }); 195 | }, 196 | 197 | createWindow(row) { 198 | var id; 199 | var primary = this.indexes[0].parts.map(p => this.fields[(p[0] == undefined) ? p.field : p[0]]); 200 | 201 | if (row) { 202 | var key = primary.map(f => row.get(f)); 203 | 204 | id = key.length == 1 ? key[0] : '[' + key.join(', ') + ']'; 205 | } 206 | 207 | var required = Ext.Array.unique(Ext.Array.flatten(this.indexes.map(index => index.parts.map(p => p[0] || p.field)))); 208 | var complexTypes = []; 209 | 210 | var items = this.format.map((field, id) => { 211 | var item = { 212 | name: field.name, 213 | xtype: 'textfield', 214 | labelAlign: 'right', 215 | fieldLabel: field.name, 216 | allowBlank: !Ext.Array.contains(required, id), 217 | flex: 1, 218 | }; 219 | 220 | if (field.type == '*') { 221 | complexTypes.push(field.name); 222 | } 223 | 224 | if ([ 'unsigned', 'UNSIGNED', 'num', 'NUM' ].indexOf(field.type) != -1) { 225 | Ext.apply(item, { 226 | xtype: 'numberfield', 227 | hideTrigger: true, 228 | }); 229 | } 230 | 231 | if ([ 'boolean', 'BOOLEAN' ].indexOf(field.type) != -1) { 232 | Ext.apply(item, { 233 | xtype: 'checkbox', 234 | }); 235 | } 236 | 237 | if (row) { 238 | item.value = row.get(field.name); 239 | 240 | if (Ext.isObject(item.value) || Ext.isArray(item.value)) { 241 | if (complexTypes.indexOf(field.name) == -1) { 242 | complexTypes.push(field.name); 243 | } 244 | 245 | item.value = Ext.JSON.encode(item.value); 246 | } 247 | 248 | if (primary.indexOf(field.name) !== -1) { 249 | item.readOnly = true; 250 | } 251 | 252 | if (item.xtype == 'numberfield' && item.value >= Math.pow(2, 32)) { 253 | item.xtype = 'textfield'; 254 | } 255 | } 256 | 257 | return item; 258 | }); 259 | 260 | var columnsCount = 1; 261 | var itemsPerColumn = items.length; 262 | 263 | while (itemsPerColumn >= 16) { 264 | itemsPerColumn /= 2; 265 | columnsCount++; 266 | } 267 | 268 | itemsPerColumn = Math.ceil(itemsPerColumn); 269 | 270 | var columns = []; 271 | 272 | if (columnsCount > 1) { 273 | var i; 274 | 275 | for (i = 0; i < columnsCount; i++) { 276 | columns.push({ 277 | border: false, 278 | flex: 1, 279 | columnWidth: .5, 280 | layout: { 281 | type: 'vbox', 282 | align: 'stretch', 283 | }, 284 | items: Ext.Array.slice(items, i*itemsPerColumn, (i+1) * itemsPerColumn), 285 | }); 286 | } 287 | } 288 | 289 | var windowTitle = row ? 'Update ' + this.params.space + ' ' + id : 'New ' + this.params.space; 290 | 291 | if (window.configuration.readOnly) { 292 | windowTitle = 'Info for ' + this.params.space + ' ' + id; 293 | } 294 | 295 | var win = Ext.create('Ext.window.Window', { 296 | title: windowTitle, 297 | modal: true, 298 | layout: 'fit', 299 | items: [ { 300 | xtype: 'form', 301 | layout: columns.length > 1 ? 'column' : { 302 | type: 'vbox', 303 | align: 'stretch', 304 | }, 305 | bodyPadding: 10, 306 | items: columns.length > 1 ? columns : items, 307 | bbar: [ '->', { 308 | text: row ? 'Update' : 'Create', 309 | formBind: true, 310 | hidden: window.configuration.readOnly, 311 | handler: () => { 312 | var job = row ? 'row.update' : 'row.create'; 313 | var currentValues = win.down('form').getValues(); 314 | var values = {}; 315 | 316 | items.forEach(item => { 317 | if (item.xtype == 'checkbox') { 318 | if (!currentValues[item.fieldLabel]) { 319 | currentValues[item.fieldLabel] = false; 320 | } 321 | } 322 | }); 323 | 324 | complexTypes.forEach(name => { 325 | try { 326 | currentValues[name] = Ext.JSON.decode(currentValues[name]); 327 | } 328 | catch (e) { 329 | currentValues[name] = null; 330 | } 331 | }); 332 | 333 | Ext.Object.each(initialValues, (k, v) => { 334 | if (v != currentValues[k]) { 335 | values[k] = v; 336 | } 337 | }); 338 | Ext.Object.each(currentValues, (k, v) => { 339 | if (v != initialValues[k]) { 340 | values[k] = v; 341 | } 342 | }); 343 | 344 | Ext.ComponentQuery.query('checkbox', win.down('form')).forEach((checkbox) => { 345 | if (Ext.isDefined(values[checkbox.name])) { 346 | values[checkbox.name] = checkbox.getValue(); 347 | } 348 | }); 349 | 350 | if (!Ext.Object.getSize(values)) { 351 | return win.close(); 352 | } 353 | 354 | if (row) { 355 | primary.forEach(f => values[f] = row.get(f)); 356 | } 357 | 358 | var params = Ext.apply({ 359 | values: values, 360 | }, this.params); 361 | 362 | dispatch(job, params).then(() => { 363 | win.close(); 364 | this.store.load(); 365 | }); 366 | }, 367 | } ], 368 | } ], 369 | }); 370 | 371 | win.show(); 372 | 373 | var initialValues = win.down('form').getValues(); 374 | }, 375 | }); 376 | -------------------------------------------------------------------------------- /public/admin/js/Space/Format.js: -------------------------------------------------------------------------------- 1 | Ext.define('Admin.Space.Format', { 2 | 3 | extend: 'Ext.grid.Panel', 4 | 5 | flex: 1, 6 | 7 | store: { 8 | fields: [ 'index', 'name', 'type', 'is_nullable' ], 9 | }, 10 | 11 | listeners: { 12 | render() { 13 | if (window.configuration.readOnly) { 14 | this.down('[text=Add]').hide(); 15 | this.down('[text=Remove]').hide(); 16 | } 17 | }, 18 | selectionchange(sm, sel) { 19 | this.down('[name=remove-button]').setDisabled(!sel.view.selection); 20 | }, 21 | }, 22 | 23 | tbar: [ { 24 | xtype: 'label', 25 | text: 'Format', 26 | }, { 27 | text: 'Add', 28 | iconCls: 'fa fa-plus-circle', 29 | handler() { 30 | var win = Ext.create('Ext.window.Window', { 31 | modal: true, 32 | title: 'New property', 33 | items: [ { 34 | xtype: 'form', 35 | bodyPadding: 10, 36 | items: [ { 37 | selectOnFocus: true, 38 | fieldLabel: 'Name', 39 | allowBlank: false, 40 | xtype: 'textfield', 41 | name: 'name', 42 | }, { 43 | fieldLabel: 'Type', 44 | allowBlank: false, 45 | name: 'type', 46 | value: 'unsigned', 47 | 48 | xtype: 'combobox', 49 | editable: false, 50 | queryMode: 'local', 51 | displayField: 'type', 52 | valueField: 'type', 53 | store: { 54 | xtype: 'arraystore', 55 | fields: [ 'type' ], 56 | data: [ 'unsigned', 'str', 'boolean', 'uuid', 'decimal', 'map', '*' ].map(v => [ v ]), 57 | }, 58 | }, { 59 | xtype: 'checkboxfield', 60 | fieldLabel: 'Is nullable', 61 | checked: false, 62 | name: 'is_nullable', 63 | } ], 64 | bbar: [ '->', { 65 | formBind: true, 66 | text: 'Create', 67 | handler: () => { 68 | var values = win.down('form').getValues(); 69 | var params = Ext.apply({ 70 | name: values.name, 71 | type: values.type, 72 | is_nullable: !!values.is_nullable, 73 | }, this.up('space-tab').params); 74 | 75 | dispatch('space.addProperty', params) 76 | .then(() => { 77 | win.close(); 78 | this.up('space-info').reloadInfo(); 79 | }); 80 | }, 81 | } ], 82 | } ], 83 | }); 84 | 85 | win.show(); 86 | win.down('textfield').focus(); 87 | }, 88 | }, { 89 | disabled: true, 90 | name: 'remove-button', 91 | iconCls: 'fa fa-minus-circle', 92 | text: 'Remove', 93 | handler() { 94 | var params = Ext.apply({ 95 | name: this.up('grid').selModel.getCellContext().view.selection.get('name'), 96 | }, this.up('space-tab').params); 97 | 98 | Ext.MessageBox.confirm( 99 | 'Danger!', 100 | 'Are you sure to drop property ' + params.name + ' in space ' + params.space + '?
This operation can not be undone', 101 | answer => { 102 | if (answer == 'yes') { 103 | dispatch('space.removeProperty', params) 104 | .then(() => { 105 | this.up('space-info').reloadInfo(); 106 | }); 107 | } 108 | } 109 | ); 110 | }, 111 | } ], 112 | 113 | selModel: { 114 | type: 'spreadsheet', 115 | rowNumbererHeaderWidth: 0, 116 | }, 117 | plugins: { 118 | ptype: 'clipboard', 119 | }, 120 | 121 | columns: [ { 122 | header: '#', 123 | width: 35, 124 | align: 'center', 125 | dataIndex: 'index', 126 | renderer: v => v + 1, 127 | }, { 128 | header: 'Name', 129 | dataIndex: 'name', 130 | flex: 1, 131 | }, { 132 | header: 'Type', 133 | dataIndex: 'type', 134 | width: 80, 135 | }, { 136 | header: 'Nullable', 137 | dataIndex: 'is_nullable', 138 | width: 80, 139 | align: 'center', 140 | renderer(v) { 141 | return v ? '' : '-'; 142 | }, 143 | }, { 144 | header: 'Reference', 145 | dataIndex: 'reference', 146 | hidden: true, 147 | flex: 1, 148 | } ], 149 | }); 150 | -------------------------------------------------------------------------------- /public/admin/js/Space/Indexes.js: -------------------------------------------------------------------------------- 1 | Ext.define('Admin.Space.Indexes', { 2 | 3 | extend: 'Ext.grid.Panel', 4 | 5 | flex: 2, 6 | 7 | statics: { 8 | iterators: [ 'EQ', 'REQ', 'ALL', 'LT', 'LE', 'GE', 'GT', 'BITS_ALL_SET', 'BITS_ANY_SET', 'BITS_ALL_NOT_SET', 'OVERLAPS', 'NEIGHBOR' ], 9 | }, 10 | 11 | store: { 12 | fields: [ 'id', 'name', 'type', 'parts', 'opts' ], 13 | idProperty: 'id', 14 | }, 15 | 16 | selModel: { 17 | type: 'spreadsheet', 18 | rowNumbererHeaderWidth: 0, 19 | }, 20 | plugins: { 21 | ptype: 'clipboard', 22 | }, 23 | 24 | listeners: { 25 | render() { 26 | if (window.configuration.readOnly) { 27 | this.down('[text=Add]').hide(); 28 | this.down('[text=Remove]').hide(); 29 | } 30 | }, 31 | selectionchange(sm, sel) { 32 | this.down('[name=remove-button]').setDisabled(!sel.view.selection); 33 | this.down('[name=search-button]').setDisabled(!sel.view.selection); 34 | }, 35 | itemdblclick() { 36 | this.down('[name=search-button]').handler(); 37 | }, 38 | }, 39 | 40 | tbar: [ { 41 | xtype: 'label', 42 | text: 'Indexes', 43 | }, { 44 | text: 'Add', 45 | iconCls: 'fa fa-plus-circle', 46 | handler() { 47 | if (this.up('space-info').down('space-format').store.getRange().length) { 48 | this.up('space-indexes').createNewIndex(); 49 | } 50 | else { 51 | return Ext.MessageBox.confirm('Error', 'No format is defined!
Do you want to continue?', (btn) => { 52 | if (btn == 'yes') { 53 | this.up('space-indexes').createNewIndex(); 54 | } 55 | }); 56 | } 57 | }, 58 | }, { 59 | text: 'Search', 60 | iconCls: 'fa fa-search', 61 | disabled: true, 62 | name: 'search-button', 63 | handler() { 64 | var params = Ext.apply({ 65 | index: this.up('space-indexes').selModel.getCellContext().view.selection.get('iid'), 66 | }, this.up('space-tab').params); 67 | 68 | var view = Ext.create('Admin.Space.Collection', { 69 | params: params, 70 | autoLoad: false, 71 | }); 72 | 73 | this.up('space-tab').add(view); 74 | this.up('space-tab').setActiveItem(view); 75 | }, 76 | }, { 77 | text: 'Remove', 78 | disabled: true, 79 | name: 'remove-button', 80 | iconCls: 'fa fa-minus-circle', 81 | handler() { 82 | var params = Ext.apply({ 83 | name: this.up('space-indexes').selModel.getCellContext().view.selection.get('name'), 84 | }, this.up('space-tab').params); 85 | 86 | var store = this.up('space-indexes').store; 87 | var recordIndex = store.findExact('name', params.name); 88 | 89 | var removeIndex = () => { 90 | dispatch('space.removeIndex', params) 91 | .then(() => { 92 | this.up('space-info').reloadInfo(); 93 | }); 94 | }; 95 | 96 | if (store.getAt(recordIndex).get('id') > 0) { 97 | removeIndex(); 98 | } 99 | else { 100 | Ext.MessageBox.confirm( 101 | 'Danger!', 102 | 'Are you sure to drop primary key ' + params.name + ' in space ' + params.space + '?
All tuples will be deleted, this operation can not be undone', 103 | answer => { 104 | if (answer == 'yes') { 105 | removeIndex(); 106 | } 107 | } 108 | ); 109 | } 110 | }, 111 | } ], 112 | 113 | columns: [ { 114 | dataIndex: 'name', 115 | header: 'Name', 116 | width: 160, 117 | }, { 118 | dataIndex: 'type', 119 | header: 'Type', 120 | align: 'center', 121 | width: 70, 122 | }, { 123 | dataIndex: 'size', 124 | header: 'Size', 125 | align: 'right', 126 | width: 70, 127 | renderer: v => v ? Ext.util.Format.fileSize(v) : '-', 128 | }, { 129 | dataIndex: 'opts', 130 | header: 'Unique', 131 | align: 'center', 132 | width: 80, 133 | renderer: v => v.unique, 134 | }, { 135 | dataIndex: 'parts', 136 | header: 'Parts', 137 | flex: 1, 138 | renderer(v) { 139 | var format = this.up('space-info').down('space-format').store; 140 | 141 | if (!format.getCount()) { 142 | return v.map(info => info[0]).join(', '); 143 | } 144 | 145 | return v.map(info => format.getAt((info[0] == undefined) ? info.field : info[0]).get('name')).join(', '); 146 | }, 147 | } ], 148 | 149 | createNewIndex() { 150 | var indexes = this; 151 | var fields = this.up('space-info').down('space-format').store.getRange(); 152 | var win = Ext.create('Ext.window.Window', { 153 | modal: true, 154 | title: 'New index', 155 | items: [ { 156 | xtype: 'form', 157 | bodyPadding: 10, 158 | defaults: { 159 | width: 300, 160 | fieldLabel: 80, 161 | }, 162 | items: [ { 163 | fieldLabel: 'Fields', 164 | xtype: 'tagfield', 165 | displayField: 'name', 166 | name: 'fields', 167 | valueField: 'index', 168 | hidden: !fields.length, 169 | store: { 170 | fields: [ 'index', 'name' ], 171 | data: fields.map((r, index) => { 172 | return { 173 | index: index, 174 | name: r.get('name'), 175 | }; 176 | }), 177 | }, 178 | listeners: { 179 | select() { 180 | var nameField = win.down('[name=name]'); 181 | var fieldsField = win.down('[name=fields]'); 182 | var format = indexes.up('space-info').down('space-format').store; 183 | 184 | if (!nameField.edited) { 185 | var name = fieldsField.value.map(i => format.getAt(i).get('name')).join('_'); 186 | 187 | nameField.setValue(name); 188 | } 189 | }, 190 | }, 191 | }, { 192 | fieldLabel: 'Name', 193 | xtype: 'textfield', 194 | allowBlank: false, 195 | name: 'name', 196 | }, { 197 | fieldLabel: 'Parts', 198 | xtype: 'textfield', 199 | name: 'parts', 200 | hidden: fields.length > 0, 201 | emptyText: '[[1, \'unsigned\'], [5, \'string\']]', 202 | }, { 203 | fieldLabel: 'Type', 204 | value: 'tree', 205 | name: 'type', 206 | xtype: 'combobox', 207 | editable: false, 208 | queryMode: 'local', 209 | displayField: 'type', 210 | valueField: 'type', 211 | store: { 212 | xtype: 'arraystore', 213 | fields: [ 'type' ], 214 | data: [ 'tree', 'hash', 'bitset', 'rtree' ].map(v => [ v ]), 215 | }, 216 | }, { 217 | xtype: 'checkboxfield', 218 | boxLabel: 'Unique index', 219 | checked: true, 220 | fieldLabel: '', 221 | name: 'unique', 222 | } ], 223 | bbar: [ '->', { 224 | formBind: true, 225 | text: 'Create', 226 | handler: () => { 227 | var values = win.down('form').getValues(); 228 | var format = indexes.up('space-info').down('space-format').store; 229 | 230 | values.fields = values.fields.map(i => format.getAt(i).get('name')); 231 | 232 | var params = Ext.apply({ 233 | name: values.name, 234 | fields: values.fields, 235 | type: values.type, 236 | parts: values.parts, 237 | unique: !!values.unique, 238 | }, this.up('space-tab').params); 239 | 240 | if (!params.fields && params.parts) { 241 | try { 242 | params.parts = Ext.JSON.decode(params.parts); 243 | } 244 | catch (e) { 245 | return Ext.MessageBox.alert('Error!', 'Invalid parts!
' + e.message); 246 | } 247 | } 248 | 249 | if (!params.parts && !params.fields) { 250 | return Ext.MessageBox.alert('Error!', 'No fields defined in index'); 251 | } 252 | 253 | dispatch('space.createIndex', params) 254 | .then(() => { 255 | win.close(); 256 | this.up('space-info').reloadInfo(); 257 | }); 258 | }, 259 | } ], 260 | } ], 261 | }); 262 | 263 | win.show(); 264 | win.down('textfield').focus(); 265 | }, 266 | }); 267 | -------------------------------------------------------------------------------- /public/admin/js/Space/Info.js: -------------------------------------------------------------------------------- 1 | Ext.define('Admin.Space.Info', { 2 | 3 | extend: 'Ext.panel.Panel', 4 | title: 'Info', 5 | 6 | requires: [ 7 | 'Admin.Space.Format', 8 | 'Admin.Space.Indexes', 9 | ], 10 | 11 | layout: 'hbox', 12 | defaults: { 13 | border: false, 14 | style: { 15 | marginLeft: '5px', 16 | marginRight: '10px', 17 | }, 18 | }, 19 | 20 | iconCls: 'fa fa-info', 21 | 22 | listeners: { 23 | activate() { 24 | this.reloadInfo(); 25 | }, 26 | }, 27 | 28 | reloadInfo() { 29 | dispatch('space.info', this.up('space-tab').params) 30 | .then(result => { 31 | result.indexes.forEach(i => delete(i.id)); 32 | this.down('space-format').store.loadData([]); 33 | 34 | if (!result.fake) { 35 | result.format.forEach((r, i) => r.index = i); 36 | this.down('space-format').store.loadData(result.format); 37 | var hasReferences = result.format.filter(f => !!f.reference).length; 38 | 39 | this.down('space-format').getColumns()[5].setHidden(!hasReferences); 40 | this.down('space-format').setWidth(hasReferences ? 380 : 300); 41 | } 42 | 43 | this.down('space-indexes').store.loadData(result.indexes); 44 | var indexSizeExists = result.indexes.filter(f => !!f.size).length; 45 | 46 | this.down('space-indexes').getColumns()[3].setHidden(!indexSizeExists); 47 | }) 48 | .catch(() => this.up('space-tab').close()); 49 | }, 50 | 51 | items: [ { 52 | xtype: 'space-format', 53 | }, { 54 | xtype: 'space-indexes', 55 | } ], 56 | }); 57 | -------------------------------------------------------------------------------- /public/admin/js/Space/Tab.js: -------------------------------------------------------------------------------- 1 | Ext.define('Admin.Space.Tab', { 2 | 3 | extend: 'Ext.tab.Panel', 4 | title: 'Space', 5 | listeners: { 6 | tabchange(tabs, tab) { 7 | var tabIndex = tabs.items.indexOf(tab); 8 | 9 | if (tabIndex == 0 || tabIndex == 1) { 10 | localStorage.setItem('space-default-item', tabIndex); 11 | } 12 | }, 13 | }, 14 | 15 | closable: true, 16 | iconCls: 'fa fa-hdd', 17 | 18 | requires: [ 19 | 'Admin.Space.Collection', 20 | 'Admin.Space.Info', 21 | ], 22 | 23 | initComponent() { 24 | this.title = this.params.space.split('_') 25 | .map(Ext.util.Format.capitalize) 26 | .join(''); 27 | 28 | this.callParent(arguments); 29 | this.setActiveTab(+localStorage.getItem('space-default-item') || 0); 30 | }, 31 | 32 | items: [ { 33 | xtype: 'space-info', 34 | }, { 35 | xtype: 'space-collection', 36 | } ], 37 | }); 38 | -------------------------------------------------------------------------------- /public/admin/js/Space/toolbar/Collection.js: -------------------------------------------------------------------------------- 1 | Ext.define('Admin.Space.toolbar.Collection', { 2 | 3 | extend: 'Ext.toolbar.Toolbar', 4 | xtype: 'toolbar-collection', 5 | 6 | initComponent() { 7 | this.items = this.getDefaultItems(); 8 | this.callParent(arguments); 9 | }, 10 | 11 | initEvents() { 12 | this.on({ 13 | destroy: this.clearRefreshInterval, 14 | scope: this, 15 | }); 16 | }, 17 | 18 | updateState() { 19 | var store = this.up('grid').store; 20 | var pageCount = Math.ceil(store.getTotalCount() / store.pageSize) || 1; 21 | var currentPage = store.currentPage; 22 | 23 | if (store.proxy.lastResponse.total == null) { 24 | this.down('[name=row-counter]').setValue('-'); 25 | this.down('[name=total-pages]').setText('-'); 26 | } 27 | else { 28 | this.down('[name=row-counter]').setValue(store.getTotalCount()); 29 | this.down('[name=total-pages]').setText(pageCount); 30 | } 31 | 32 | var first = this.down('[name=first-page]'); 33 | var prev = this.down('[name=previous-page]'); 34 | var current = this.down('[name=current-page]'); 35 | var next = this.down('[name=next-page]'); 36 | var last = this.down('[name=last-page]'); 37 | 38 | first.setDisabled(currentPage == 1); 39 | prev.setDisabled(currentPage == 1); 40 | current.setValue(currentPage); 41 | next.setDisabled(currentPage == pageCount); 42 | last.setDisabled(currentPage == pageCount); 43 | 44 | // unknown row count 45 | if (store.proxy.lastResponse.total == null) { 46 | next.setDisabled(!store.proxy.lastResponse.next); 47 | last.setDisabled(true); 48 | } 49 | }, 50 | 51 | refreshStore() { 52 | if (!this.up('grid').isVisible()) { 53 | // grid is not active 54 | return; 55 | } 56 | 57 | var spaceTab = this.up('grid').up('tabpanel'); 58 | 59 | if (!spaceTab.isVisible()) { 60 | // space tab is not active 61 | return; 62 | } 63 | 64 | var databaseTab = spaceTab.up('tabpanel'); 65 | 66 | if (!databaseTab.isVisible()) { 67 | // database tab is not active 68 | return; 69 | } 70 | 71 | this.down('[name=refresh]').blur(); 72 | 73 | return this.up('grid').store.load(); 74 | }, 75 | 76 | setRefreshMode(text) { 77 | this.clearRefreshInterval(); 78 | this.refreshStore(); 79 | 80 | if (text != 'Manual') { 81 | this.down('[name=refresh]').setText(text); 82 | var seconds = 0; 83 | 84 | if (text.indexOf('second') !== -1) { 85 | seconds = 1; 86 | } 87 | else if (text.indexOf('minute') === -1) { 88 | throw 'invalid text ' + text; 89 | } 90 | else { 91 | seconds = 60; 92 | } 93 | 94 | var amount = +text.split(' ')[1]; 95 | 96 | this.refreshInterval = setInterval(this.refreshStore.bind(this), amount * seconds * 1000); 97 | } 98 | }, 99 | 100 | clearRefreshInterval() { 101 | if (this.refreshInterval) { 102 | clearInterval(this.refreshInterval); 103 | this.refreshInterval = null; 104 | } 105 | 106 | if (!this.destroying) { 107 | this.down('[name=refresh]').setText(''); 108 | } 109 | }, 110 | 111 | getDefaultItems() { 112 | return [ { 113 | text: 'Create', 114 | iconCls: 'fa fa-plus-circle', 115 | handler() { 116 | this.up('grid').createWindow(); 117 | }, 118 | }, { 119 | text: 'Update', 120 | iconCls: 'fa fa-edit', 121 | disabled: true, 122 | handler() { 123 | var selected = this.up('grid') 124 | .getSelectionModel() 125 | .getSelected(); 126 | var record = selected.startCell ? selected.startCell.record : selected.selectedRecords.items[0]; 127 | 128 | this.up('grid').createWindow(record); 129 | }, 130 | }, { 131 | text: 'Delete', 132 | disabled: true, 133 | iconCls: 'fa fa-minus-circle', 134 | handler() { 135 | var grid = this.up('grid'); 136 | var selected = grid.getSelectionModel().getSelected(); 137 | var records = []; 138 | 139 | if (selected.startCell) { 140 | for (var i = selected.startCell.rowIdx; i <= selected.endCell.rowIdx; i++) { 141 | records.push(grid.store.getAt(i)); 142 | } 143 | } 144 | else { 145 | records = selected.selectedRecords.items; 146 | } 147 | 148 | var msg = 'Are you sure want to delete selected row?'; 149 | 150 | if (records.length > 1) { 151 | msg = 'Are you sure want to delete ' + records.length + ' selected rows?'; 152 | } 153 | 154 | Ext.MessageBox.confirm('Warning', msg, (answer) => { 155 | if (answer == 'no') { 156 | return; 157 | } 158 | 159 | var params = records.map(record => { 160 | var id = {}; 161 | 162 | grid.indexes[0].parts.forEach(p => { 163 | id[grid.fields[(p[0] == undefined) ? p.field : p[0]]] = record.get(grid.fields[(p[0] == undefined) ? p.field : p[0]]); 164 | }); 165 | return Ext.apply({ id: id }, grid.params); 166 | }); 167 | 168 | return dispatch.progress('row.remove', params) 169 | .then(() => this.up('toolbar-collection').refreshStore()); 170 | }); 171 | }, 172 | }, { 173 | text: 'Search', 174 | iconCls: 'fa fa-search', 175 | name: 'search', 176 | disabled: true, 177 | menu: [], 178 | }, { 179 | text: this.params.truncateButtonText || 'Truncate', 180 | name: 'truncate', 181 | iconCls: 'fa fa-trash', 182 | handler() { 183 | var params = this.up('grid').store.proxy.params; 184 | var space = params.space; 185 | 186 | // > database tabs 187 | // > collection 188 | // > space tabs 189 | // > {collection} 190 | 191 | var index = this.up('grid').indexes[params.index]; 192 | var searchdata = { 193 | key: params.key, 194 | index: params.index, 195 | indexObj: index, 196 | iterator: params.iterator }; 197 | 198 | this.up('tabpanel').up('tabpanel') 199 | .down('[name=spaces]') 200 | .truncateSpace(space, searchdata); 201 | 202 | this.up('toolbar-collection').refreshStore(); 203 | }, 204 | }, { 205 | iconCls: 'fa fa-download', 206 | disabled: true, 207 | name: 'export', 208 | text: 'Export', 209 | handler() { 210 | dispatch('export.csv', this.up('grid').store.proxy.params) 211 | .then(result => window.location = '/' + result.path); 212 | }, 213 | }, '->', { 214 | fieldLabel: 'Total rows', 215 | labelAlign: 'right', 216 | labelWidth: 65, 217 | name: 'row-counter', 218 | readOnly: true, 219 | width: 20, 220 | xtype: 'textfield', 221 | }, ' ', { 222 | xtype: 'numberfield', 223 | minValue: 1, 224 | value: 25, 225 | width: 20, 226 | labelWidth: 65, 227 | labelAlign: 'right', 228 | fieldLabel: 'Page size', 229 | selectOnFocus: true, 230 | name: 'pageSize', 231 | hideTrigger: true, 232 | keyNavEnabled: false, 233 | listeners: { 234 | change(field, v) { 235 | if (!v) { 236 | return this.setValue(1); 237 | } 238 | 239 | var store = this.up('grid').store; 240 | 241 | if (store.pageSize != v) { 242 | store.setPageSize(v); 243 | store.loadPage(1); 244 | } 245 | 246 | localStorage.setItem('admin-page-size', v); 247 | 248 | if (v == 25 || !v) { 249 | localStorage.removeItem('admin-page-size'); 250 | } 251 | }, 252 | }, 253 | }, ' ', { 254 | iconCls: 'fa fa-backward', 255 | name: 'first-page', 256 | handler() { 257 | this.up('grid').store.loadPage(1); 258 | }, 259 | }, { 260 | iconCls: 'fa fa-chevron-left', 261 | name: 'previous-page', 262 | handler() { 263 | this.up('grid').store.previousPage(); 264 | }, 265 | }, { 266 | xtype: 'numberfield', 267 | name: 'current-page', 268 | width: 20, 269 | hideTrigger: true, 270 | keyNavEnabled: false, 271 | labelWidth: 40, 272 | labelAlign: 'right', 273 | fieldLabel: 'Page', 274 | selectOnFocus: true, 275 | value: 1, 276 | enableKeyEvents: true, 277 | listeners: { 278 | buffer: 500, 279 | keyup(field) { 280 | var store = this.up('grid').store; 281 | var pageCount = Math.ceil(store.getTotalCount() / store.pageSize) || 1; 282 | 283 | if (store.proxy.lastResponse.total == null) { 284 | this.up('grid').store.loadPage(field.value || 1); 285 | } 286 | else if (field.value <= pageCount && field.value >= 0) { 287 | this.up('grid').store.loadPage(field.value || 1); 288 | } 289 | else if (field.value > pageCount) { 290 | this.up('grid').store.loadPage(pageCount); 291 | } 292 | else if (field.value < 0) { 293 | this.up('grid').store.loadPage(1); 294 | } 295 | }, 296 | }, 297 | }, { 298 | xtype: 'label', 299 | name: 'current-page-delimiter', 300 | text: '/', 301 | }, { 302 | xtype: 'label', 303 | name: 'total-pages', 304 | text: '1', 305 | }, { 306 | iconCls: 'fa fa-chevron-right', 307 | name: 'next-page', 308 | handler() { 309 | this.up('grid').store.nextPage(); 310 | }, 311 | }, { 312 | iconCls: 'fa fa-forward', 313 | name: 'last-page', 314 | handler() { 315 | this.up('grid').store.loadPage(this.up('grid').down('[name=total-pages]').text); 316 | }, 317 | }, { 318 | xtype: 'splitbutton', 319 | name: 'refresh', 320 | iconCls: 'fa fa-sync', 321 | handler() { 322 | this.up('toolbar-collection').setRefreshMode('Manual'); 323 | this.down('[text=Manual]').setChecked(true); 324 | }, 325 | menu: { 326 | defaults: { 327 | xtype: 'menucheckitem', 328 | group: 'refresh-mode', 329 | hideOnClick: true, 330 | handler() { 331 | this.up('toolbar-collection').setRefreshMode(this.text); 332 | }, 333 | }, 334 | items: [ { 335 | text: 'Manual', 336 | checked: true, 337 | }, { 338 | text: 'Every 1 second', 339 | }, { 340 | text: 'Every 2 seconds', 341 | }, { 342 | text: 'Every 5 seconds', 343 | }, { 344 | text: 'Every 15 seconds', 345 | }, { 346 | text: 'Every 30 seconds', 347 | }, { 348 | text: 'Every 1 minute', 349 | }, { 350 | text: 'Every 2 minutes', 351 | }, { 352 | text: 'Every 5 minutes', 353 | }, { 354 | text: 'Every 15 minutes', 355 | }, { 356 | text: 'Every 30 minutes', 357 | } ], 358 | }, 359 | } ]; 360 | }, 361 | 362 | applyMeta() { 363 | var indexMenu = this.up('grid').indexes.map(index => { 364 | return { 365 | text: index.name, 366 | handler: () => { 367 | var params = Ext.apply({ index: index.id, truncateButtonText: 'Truncate rows' }, this.up('space-tab').params); 368 | var view = Ext.create('Admin.Space.Collection', { 369 | params: params, 370 | autoLoad: false, 371 | }); 372 | 373 | this.up('space-tab').add(view); 374 | this.up('space-tab').setActiveItem(view); 375 | }, 376 | }; 377 | }); 378 | 379 | var search = this.down('[name=search]'); 380 | 381 | if (indexMenu.length) { 382 | search.setMenu(indexMenu); 383 | search.enable(); 384 | } 385 | }, 386 | }); 387 | -------------------------------------------------------------------------------- /public/admin/js/Space/toolbar/Search.js: -------------------------------------------------------------------------------- 1 | Ext.define('Admin.Space.toolbar.Search', { 2 | extend: 'Ext.toolbar.Toolbar', 3 | border: false, 4 | 5 | initComponent() { 6 | var grid = this.collection; 7 | var index = grid.indexes.filter(i => i.id == grid.params.index)[0]; 8 | 9 | var camelCasedName = index.name.split('_') 10 | .map(Ext.util.Format.capitalize) 11 | .join(''); 12 | 13 | grid.setTitle('Index: ' + camelCasedName); 14 | 15 | var items = [ { 16 | xtype: 'label', 17 | text: 'Index params', 18 | style: { 19 | marginLeft: '5px', 20 | marginRight: '15px', 21 | }, 22 | }, ' ' ]; 23 | 24 | index.parts.forEach(p => { 25 | var partName = grid.fields[p.field === undefined ? p[0] : p.field]; 26 | 27 | if (p.path) { 28 | if (!p.path.startsWith('.')) { 29 | partName += '.'; 30 | } 31 | 32 | partName += p.path; 33 | } 34 | 35 | items.push({ 36 | xtype: 'label', 37 | text: partName, 38 | }); 39 | 40 | var field = { 41 | xtype: 'textfield', 42 | searchField: true, 43 | }; 44 | 45 | if ([ 'str', 'string' ].indexOf((p[1] || p.type).toLowerCase()) == -1) { 46 | Ext.apply(field, { 47 | xtype: 'numberfield', 48 | hideTrigger: true, 49 | minValue: 0, 50 | }); 51 | } 52 | 53 | items.push(Ext.apply(field, { 54 | name: partName, 55 | width: 70, 56 | labelAlign: 'right', 57 | enableKeyEvents: true, 58 | listeners: { 59 | specialkey(field, e) { 60 | if (e.getKey() == e.ENTER) { 61 | field.up('space-collection') 62 | .down('[text=EQ]') 63 | .handler(); 64 | } 65 | }, 66 | }, 67 | })); 68 | }); 69 | 70 | items.push({ 71 | text: 'Select', 72 | iconCls: 'fa fa-search', 73 | menu: window.Admin.Space.Indexes.iterators.map((text, iterator) => { 74 | return { 75 | text: text, 76 | handler: () => { 77 | grid.down('[text=' + text +']') 78 | .up('button') 79 | .setText(text + ' iterator'); 80 | var params = []; 81 | 82 | this.items.findBy(item => { 83 | if (item.searchField) { 84 | if (item.value === '' || item.value === undefined) { 85 | return true; 86 | } 87 | 88 | params.push(item.value); 89 | } 90 | }); 91 | grid.store.proxy.params.key = [ 0 ]; 92 | grid.store.proxy.params.iterator = iterator; 93 | 94 | grid.store.proxy.params.key = params; 95 | grid.store.load(); 96 | }, 97 | }; 98 | }), 99 | }); 100 | 101 | this.items = items; 102 | this.callParent(arguments); 103 | 104 | setTimeout(() => this.down('textfield').focus(), 100); 105 | }, 106 | }); 107 | -------------------------------------------------------------------------------- /public/admin/js/Viewport.js: -------------------------------------------------------------------------------- 1 | Ext.define('Admin.Viewport', { 2 | 3 | extend: 'Ext.Viewport', 4 | 5 | requires: [ 6 | 'Admin.Home.Tab', 7 | 'Admin.field.Filter', 8 | 'Admin.overrides.Toolbar', 9 | ], 10 | 11 | initComponent() { 12 | window.dispatch = this.dispatch.bind(this); 13 | window.dispatch.progress = this.dispatchProgress.bind(this); 14 | this.callParent(arguments); 15 | }, 16 | 17 | layout: 'border', 18 | items: [ { 19 | region: 'center', 20 | xtype: 'tabpanel', 21 | border: false, 22 | layout: 'fit', 23 | items: [ { 24 | xtype: 'home-tab', 25 | } ], 26 | tabBar: { 27 | items: [ { 28 | xtype: 'tbfill', 29 | }, { 30 | xtype: 'button', 31 | name: 'version', 32 | baseCls: 'version-button', 33 | handler() { 34 | window.open('https://github.com/basis-company/tarantool-admin/releases'); 35 | }, 36 | } ], 37 | }, 38 | } ], 39 | 40 | dispatch(job, params, silent = false) { 41 | params = params || {}; 42 | 43 | if (!silent) { 44 | var el = (Ext.WindowManager.getActive() || this).el; 45 | var timeout = setTimeout(function() { 46 | el.mask('Please, wait'); 47 | }, 250); 48 | } 49 | 50 | return new Promise(function(resolve, reject) { 51 | Ext.Ajax.request({ 52 | method: 'post', 53 | url: '/admin/api', 54 | params: { 55 | rpc: Ext.JSON.encode({ 56 | job: job, 57 | params: params, 58 | }), 59 | }, 60 | success: response => { 61 | clearTimeout(timeout); 62 | 63 | try { 64 | var result = Ext.JSON.decode(response.responseText); 65 | } 66 | catch (e) { 67 | result = { 68 | success: false, 69 | message: e, 70 | }; 71 | } 72 | 73 | if (!silent && el.isMasked()) { 74 | setTimeout(function() { 75 | el.unmask(); 76 | }, 250); 77 | } 78 | 79 | if (result.success) { 80 | resolve(result.data || {}); 81 | } 82 | else { 83 | Ext.MessageBox.alert('Error', result.message); 84 | reject(result.message); 85 | } 86 | }, 87 | }); 88 | }); 89 | }, 90 | 91 | dispatchProgress(job, data) { 92 | Ext.MessageBox.show({ 93 | title: 'Processing', 94 | closable: false, 95 | modal: true, 96 | progress: true, 97 | width: 450, 98 | }); 99 | 100 | var results = []; 101 | 102 | var promise = Promise.resolve(); 103 | 104 | data.forEach((params, i) => { 105 | promise = promise.then(() => { 106 | if (!Ext.MessageBox.progressBar) { 107 | return; 108 | } 109 | 110 | var value = i / data.length; 111 | var text = i + ' / ' + data.length; 112 | 113 | if (!Ext.MessageBox.progressBar.isVisible()) { 114 | return; 115 | } 116 | 117 | Ext.MessageBox.progressBar.updateProgress(value, text, true); 118 | 119 | return this.dispatch(job, params, true) 120 | .then(result => { 121 | results.push(result); 122 | return results; 123 | }); 124 | }); 125 | }); 126 | promise.then(function() { 127 | Ext.MessageBox.close(); 128 | }); 129 | return promise; 130 | }, 131 | }); 132 | -------------------------------------------------------------------------------- /public/admin/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | Ext.onReady(() => { 2 | Ext.Loader.setConfig({ 3 | enabled: true, 4 | disableCaching: true, 5 | paths: { 6 | Admin: '/admin/js', 7 | }, 8 | }); 9 | 10 | Ext.define('Basis.override.Ext', { 11 | override: 'Ext', 12 | define(className, data) { 13 | if (!data.xtype && className) { 14 | data.xtype = Ext.Array.splice(className.split('.'), 1) 15 | .join('-') 16 | .toLowerCase(); 17 | } 18 | 19 | var Manager = Ext.ClassManager; 20 | 21 | Ext.classSystemMonitor && Ext.classSystemMonitor(className, 'ClassManager#define', arguments); 22 | 23 | if (data.override) { 24 | Manager.classState[className] = 20; 25 | return Manager.createOverride.apply(Manager, arguments); 26 | } 27 | 28 | Manager.classState[className] = 10; 29 | return Manager.create.apply(Manager, arguments); 30 | }, 31 | }); 32 | 33 | Ext.create('Admin.Viewport'); 34 | }); 35 | -------------------------------------------------------------------------------- /public/admin/js/data/proxy/PagingDispatch.js: -------------------------------------------------------------------------------- 1 | Ext.define('Admin.data.proxy.PagingDispatch', { 2 | 3 | extend: 'Ext.data.proxy.Memory', 4 | alias: 'proxy.pagingdispatch', 5 | 6 | job: null, 7 | 8 | read: function(operation) { 9 | var params = Ext.apply({}, this.params, { 10 | limit: operation.getLimit(), 11 | offset: operation.getStart(), 12 | }); 13 | 14 | dispatch(this.job, params) 15 | .then(response => { 16 | this.lastResponse = response; 17 | var resultSet = new Ext.data.ResultSet({ 18 | total: response.total, 19 | count: (response.data || []).length, 20 | records: (response.data || []).map(row => { 21 | Ext.Array.insert(row, 0, [ Ext.id() ]); 22 | return operation._scope.model.create(row); 23 | }), 24 | }); 25 | 26 | operation.setResultSet(resultSet); 27 | operation.setSuccessful(true); 28 | }); 29 | }, 30 | }); 31 | -------------------------------------------------------------------------------- /public/admin/js/field/Filter.js: -------------------------------------------------------------------------------- 1 | Ext.define('Admin.field.Filter', { 2 | 3 | extend: 'Ext.form.field.Text', 4 | xtype: 'filter-field', 5 | emptyText: 'filter', 6 | enableKeyEvents: true, 7 | 8 | listeners: { 9 | buffer: 100, 10 | keyup() { 11 | var store = this.up('grid').store; 12 | 13 | store.applyFilters(store.filters.items); 14 | }, 15 | render() { 16 | this.focus(); 17 | this.up('grid').store.addFilter((record) => { 18 | if (!this.value) { 19 | return true; 20 | } 21 | 22 | return window.Admin.field.Filter.recordContainsText(record, this.value); 23 | }); 24 | }, 25 | }, 26 | 27 | statics: { 28 | charMap: { 29 | ru: { 30 | 'q': 'й', 'w': 'ц', 'e': 'у', 'r': 'к', 't': 'е', 'y': 'н', 'u': 'г', 'i': 'ш', 'o': 'щ', 'p': 'з', '[': 'х', 31 | ']': 'ъ', 'a': 'ф', 's': 'ы', 'd': 'в', 'f': 'а', 'g': 'п', 'h': 'р', 'j': 'о', 'k': 'л', 'l': 'д', ';': 'ж', 32 | '\'': 'э', 'z': 'я', 'x': 'ч', 'c': 'с', 'v': 'м', 'b': 'и', 'n': 'т', 'm': 'ь', ',': 'б', '.': 'ю', '/': '.', 33 | }, 34 | }, 35 | recordContainsText(record, text) { 36 | if (!window.Admin.field.Filter.charMap.en) { 37 | window.Admin.field.Filter.charMap.en = {}; 38 | Ext.Object.each(window.Admin.field.Filter.charMap.ru, (k, v) => window.Admin.field.Filter.charMap.en[v] = k); 39 | } 40 | 41 | var result = false; 42 | var lowercase = Ext.util.Format.lowercase; 43 | var textInv = window.Admin.field.Filter.translate(text, 'ru', 'en'); 44 | var textRu = window.Admin.field.Filter.translate(text, 'ru'); 45 | var textEn = window.Admin.field.Filter.translate(text, 'en'); 46 | 47 | if (!text) { 48 | return true; 49 | } 50 | 51 | Ext.Object.each(record.data, (k, v) => { 52 | return !(result = (lowercase(v).indexOf(text) !== -1 || lowercase(v).indexOf(textInv) !== -1 || 53 | lowercase(v).indexOf(textRu) !== -1 || lowercase(v).indexOf(textEn) !== -1)); 54 | }); 55 | 56 | return result; 57 | }, 58 | 59 | translate(text, lang1, lang2) { 60 | for (var i=0; i < text.length; i++) { 61 | if (window.Admin.field.Filter.charMap[lang1][text[i]]) { 62 | text = text.replace(text[i], window.Admin.field.Filter.charMap[lang1][text[i]]); 63 | } 64 | else if (lang2 && window.Admin.field.Filter.charMap[lang2][text[i]]) { 65 | text = text.replace(text[i], window.Admin.field.Filter.charMap[lang2][text[i]]); 66 | } 67 | } 68 | 69 | return text; 70 | }, 71 | }, 72 | }); 73 | -------------------------------------------------------------------------------- /public/admin/js/overrides/Toolbar.js: -------------------------------------------------------------------------------- 1 | Ext.define('Admin.overrides.Toolbar', { 2 | 3 | override: 'Ext.toolbar.Toolbar', 4 | 5 | lookupComponent: function(c) { 6 | if (Ext.isObject(c)) { 7 | c.grow = true; 8 | 9 | if (c.width) { 10 | c.growMin = c.width; 11 | delete c.width; 12 | } 13 | else { 14 | c.growMin = 160; 15 | } 16 | } 17 | 18 | return this.callParent(arguments); 19 | }, 20 | }); 21 | -------------------------------------------------------------------------------- /public/admin/style.css: -------------------------------------------------------------------------------- 1 | .fa, 2 | .far { 3 | font-size: 14px; 4 | } 5 | .x-tab.x-tab-active.x-tab-default { 6 | border-color: #aaa; 7 | background-color: #ddd; 8 | } 9 | 10 | .x-tab-default-top { 11 | border-radius: 0px; 12 | background-color: transparent; 13 | } 14 | 15 | .x-tab.x-tab-active.x-tab-default .x-tab-inner-default, 16 | .x-tab.x-tab-active.x-tab-default .x-tab-icon-el { 17 | color: #000; 18 | } 19 | 20 | .-window-header-title-default, 21 | .x-panel-header-title-default { 22 | color: #555; 23 | } 24 | 25 | .x-toolbar-default { 26 | border: none; 27 | } 28 | .x-btn-default-toolbar-small { 29 | border-radius: 0px; 30 | border-color: white; 31 | background-color: white; 32 | } 33 | 34 | .x-btn-icon-el-default-toolbar-small { 35 | padding-top: 0px; 36 | } 37 | .x-btn.x-btn-disabled.x-btn-default-toolbar-small { 38 | background-color: #fff; 39 | opacity: 0.35; 40 | } 41 | 42 | .x-grid-with-col-lines .x-grid-cell, 43 | .x-grid-with-row-lines .x-grid-item { 44 | border-width: 0px; 45 | } 46 | 47 | .x-column-header { 48 | border-color: #fff; 49 | background-color: #f0f0f0; 50 | border-left-style: solid; 51 | color: #7e7e7e; 52 | font-size: 10px; 53 | } 54 | .x-grid-cell-special { 55 | border-right-width: 0px; 56 | } 57 | .x-grid-cell-inner-row-numberer { 58 | background-color: #f0f0f0; 59 | color: #7e7e7e; 60 | font-size: 10px; 61 | border-top: 1px solid white; 62 | border-right: 1px solid white; 63 | } 64 | .x-column-header-inner { 65 | padding-top: 3px; 66 | padding-bottom: 3px; 67 | } 68 | .x-grid-header-ct { 69 | border-color: white; 70 | } 71 | .x-grid-body { 72 | border: 0px; 73 | } 74 | .x-grid-with-col-lines .x-grid-cell, 75 | .x-grid-with-row-lines .x-grid-item { 76 | padding-bottom: 0px; 77 | } 78 | 79 | .x-grid-with-row-lines .x-grid-item:last-child { 80 | border-bottom-width: 0px; 81 | } 82 | 83 | .x-form-trigger-wrap-default { 84 | padding-top: 1px; 85 | border-width: 0px 0px 1px 0px; 86 | } 87 | .query-textarea .x-form-trigger-wrap-default { 88 | border-width: 1px; 89 | } 90 | 91 | .x-panel-body-default { 92 | border: 0px; 93 | } 94 | .x-tabpanel-body-default { 95 | border-top: 1px; 96 | } 97 | .x-tabpanel-child { 98 | border-top: 1px solid #ddd; 99 | } 100 | .x-btn.x-btn-disabled.x-btn-default-small { 101 | background-color: #999; 102 | } 103 | .x-btn-default-small { 104 | border-color: white; 105 | border-radius: 0px; 106 | background-color: #999; 107 | } 108 | .version-button { 109 | border-radius: 0px; 110 | border: 1px solid #f5f5f5; 111 | background-color: #f5f5f5; 112 | } 113 | .version-button:hover { 114 | cursor: pointer; 115 | border: 1px solid #c9deed; 116 | background-color: #c9deed; 117 | } 118 | .version-button:hover span{ 119 | color: #000; 120 | } 121 | .version-button { 122 | color: #999; 123 | padding: 0px 2px 0px 2px; 124 | } 125 | .version-button .x-btn-icon-el { 126 | margin-right: 2px; 127 | } 128 | .version-button span { 129 | color: #999; 130 | } 131 | .version-upgrade span { 132 | color: #696; 133 | } 134 | -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | tarantool admin <?php echo $version; ?> 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /public/server.php: -------------------------------------------------------------------------------- 1 | job || !$parsed->params) { 18 | throw new Exception("Invalid rpc format"); 19 | } 20 | $parts = explode('.', 'job.'.$parsed->job); 21 | $uppercased = array_map('ucfirst', $parts); 22 | $class = implode('\\', $uppercased); 23 | 24 | if (!class_exists($class)) { 25 | throw new Exception("Invalid rpc.job value"); 26 | } 27 | 28 | $instance = new $class; 29 | 30 | foreach ($parsed->params as $k => $v) { 31 | $instance->$k = $v; 32 | } 33 | 34 | $result = $instance->run(); 35 | header('Content-Type: application/json'); 36 | echo json_encode(['success' => true, 'data' => $result]); 37 | } catch (Exception $e) { 38 | echo $e->getMessage(); 39 | } 40 | -------------------------------------------------------------------------------- /var/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/basis-company/tarantool-admin/491435ffbcfbea21e1d4f8877de487debe31d0a9/var/.gitkeep --------------------------------------------------------------------------------