├── .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 [](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 |
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
' +
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 |