├── .dockerignore
├── .editorconfig
├── .eslintrc.json
├── .githooks
└── pre-push
├── .gitignore
├── .travis.yml
├── LICENSE
├── Makefile
├── README.md
├── angular.json
├── db.js
├── dist
└── qan-app
│ ├── 3rdpartylicenses.txt
│ ├── assets
│ ├── 50747258.png
│ ├── Percona_XtraDB_Cluster.png
│ ├── background.png
│ ├── database.png
│ ├── footer-logo.png
│ ├── logo.png
│ ├── mysql_64_black.png
│ ├── percona-server-black-50.png
│ ├── pmm-logo.svg
│ └── sm-logo.png
│ ├── favicon.ico
│ ├── fontawesome-webfont.674f50d287a8c48dc19b.eot
│ ├── fontawesome-webfont.912ec66d7572ff821749.svg
│ ├── fontawesome-webfont.af7ae505a9eed503f8b8.woff2
│ ├── fontawesome-webfont.b06871f281fee6b241d6.ttf
│ ├── fontawesome-webfont.fee66e712a8a08eef580.woff
│ ├── footer-logo.f5f92e041aab6e0a0731.png
│ ├── index.html
│ ├── main.ee6df98d405bc48974d9.js
│ ├── pmm-logo.774290ea45f094ac7586.svg
│ ├── polyfills-es5.e492bfb9ad643cb45862.js
│ ├── polyfills.9f7a0b0d8e877fe82e43.js
│ ├── runtime.458556a34b891ea32398.js
│ └── styles.83d91574d2ae0db906c8.css
├── e2e
├── app.e2e-spec.ts
├── app.po.ts
└── tsconfig.e2e.json
├── jsons
├── 1.json
└── qan-api
│ └── instances.json
├── karma.conf.js
├── package-lock.json
├── package.json
├── protractor-ci.conf.js
├── protractor.conf.js
├── proxy.conf.e2e.json
├── proxy.conf.json
├── routes.json
├── src
├── app
│ ├── add-amazon-rds
│ │ ├── add-amazon-rds.component.html
│ │ ├── add-amazon-rds.component.scss
│ │ ├── add-amazon-rds.component.spec.ts
│ │ ├── add-amazon-rds.component.ts
│ │ ├── add-amazon-rds.service.spec.ts
│ │ └── add-amazon-rds.service.ts
│ ├── add-instance
│ │ ├── add-instance.component.html
│ │ ├── add-instance.component.scss
│ │ ├── add-instance.component.spec.ts
│ │ └── add-instance.component.ts
│ ├── add-remote-instances
│ │ ├── add-remote-instance.component.html
│ │ ├── add-remote-instance.component.scss
│ │ ├── add-remote-instance.component.ts
│ │ └── add-remote-instance.service.ts
│ ├── app-routing.module.ts
│ ├── app.component.html
│ ├── app.component.scss
│ ├── app.component.spec.ts
│ ├── app.component.ts
│ ├── app.module.ts
│ ├── core
│ │ ├── core.component.ts
│ │ ├── core.module.ts
│ │ ├── instance.service.spec.ts
│ │ ├── instance.service.ts
│ │ ├── json-tree
│ │ │ ├── json-tree.component.html
│ │ │ ├── json-tree.component.scss
│ │ │ └── json-tree.component.ts
│ │ ├── nav
│ │ │ ├── nav.component.html
│ │ │ ├── nav.component.scss
│ │ │ ├── nav.component.spec.ts
│ │ │ └── nav.component.ts
│ │ ├── page-not-found
│ │ │ ├── page-not-found.component.html
│ │ │ ├── page-not-found.component.scss
│ │ │ ├── page-not-found.component.spec.ts
│ │ │ └── page-not-found.component.ts
│ │ └── qan-error.handler.ts
│ ├── environment.ts
│ ├── mongo-query-details
│ │ ├── mongo-query-details.component.html
│ │ ├── mongo-query-details.component.scss
│ │ ├── mongo-query-details.component.spec.ts
│ │ ├── mongo-query-details.component.ts
│ │ ├── mongo-query-details.service.spec.ts
│ │ └── mongo-query-details.service.ts
│ ├── mysql-query-details
│ │ ├── mysql-query-details.component.html
│ │ ├── mysql-query-details.component.scss
│ │ ├── mysql-query-details.component.spec.ts
│ │ ├── mysql-query-details.component.ts
│ │ ├── mysql-query-details.service.spec.ts
│ │ └── mysql-query-details.service.ts
│ ├── query-profile
│ │ ├── query-profile.component.html
│ │ ├── query-profile.component.scss
│ │ ├── query-profile.component.spec.ts
│ │ ├── query-profile.component.ts
│ │ ├── query-profile.service.spec.ts
│ │ └── query-profile.service.ts
│ ├── remote-instances-list
│ │ ├── remote-instances-list.component.html
│ │ ├── remote-instances-list.component.scss
│ │ ├── remote-instances-list.component.spec.ts
│ │ ├── remote-instances-list.component.ts
│ │ ├── remote-instances-list.service.spec.ts
│ │ └── remote-instances-list.service.ts
│ ├── settings
│ │ ├── settings.component.html
│ │ ├── settings.component.scss
│ │ ├── settings.component.spec.ts
│ │ ├── settings.component.ts
│ │ ├── settings.service.spec.ts
│ │ └── settings.service.ts
│ ├── shared
│ │ ├── humanize.pipe.spec.ts
│ │ ├── humanize.pipe.ts
│ │ ├── latency-chart.directive.spec.ts
│ │ ├── latency-chart.directive.ts
│ │ ├── load-sparklines.directive.spec.ts
│ │ ├── load-sparklines.directive.ts
│ │ ├── map-to-iterable.pipe.spec.ts
│ │ ├── map-to-iterable.pipe.ts
│ │ ├── moment-format.pipe.spec.ts
│ │ ├── moment-format.pipe.ts
│ │ ├── parse-query-param-date.pipe.spec.ts
│ │ ├── parse-query-param-date.pipe.ts
│ │ ├── shared.module.ts
│ │ ├── sorting-table.pipe.spec.ts
│ │ ├── sorting-table.pipe.ts
│ │ ├── truncate-root.pipe.spec.ts
│ │ └── truncate-root.pipe.ts
│ └── summary
│ │ ├── summary.component.html
│ │ ├── summary.component.scss
│ │ ├── summary.component.spec.ts
│ │ ├── summary.component.ts
│ │ ├── summary.service.spec.ts
│ │ └── summary.service.ts
├── assets
│ ├── .gitkeep
│ ├── 50747258.png
│ ├── Percona_XtraDB_Cluster.png
│ ├── background.png
│ ├── database.png
│ ├── footer-logo.png
│ ├── logo.png
│ ├── mysql_64_black.png
│ ├── percona-server-black-50.png
│ ├── pmm-logo.svg
│ └── sm-logo.png
├── environments
│ ├── environment.prod.ts
│ └── environment.ts
├── favicon.ico
├── index.html
├── main.ts
├── polyfills.ts
├── styles.scss
├── test.ts
├── tsconfig.app.json
├── tsconfig.spec.json
└── typings.d.ts
├── tsconfig.json
└── tslint.json
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .git
3 | .vscode/
4 | .DS_Store
5 | package-lock.json
6 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.md]
12 | max_line_length = off
13 | trim_trailing_whitespace = false
14 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parserOptions": {
3 | "ecmaVersion": 6
4 | },
5 | "rules": {
6 | "quotes": ["off"]
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/.githooks/pre-push:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # This pre-post git hook makes only basic check of a build.
3 | # Please double check if you made a build over all you changes and committed it.
4 | # Use --no-verify flag on your risk.
5 |
6 | # checks if dist/qan-app/ is the newest againts node_modules/ and src/ folders.
7 | if [ `stat -f "%m" dist/qan-app/` -ge `stat -f "%m" node_modules/` -a `stat -f "%m" dist/qan-app/` -ge `stat -f "%m" src/` ] ; then
8 | # checks if dist/qan-app/ id added and commited.
9 | if git status | grep dist/qan-app/ ; then
10 | echo "Please add and commit dist/qan-app/ before push."
11 | exit 1
12 | else
13 | echo "Looks like dist/qan-app/ is up to date. Pushing to origin..."
14 | exit 0
15 | fi
16 | else
17 | echo "Looks like dist/qan-app/ is NOT up to date."
18 | echo "Please do npm run build, add dist/qan-app/ and commit."
19 | exit 1
20 | fi
21 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /tmp
5 |
6 | # dependencies
7 | /node_modules
8 |
9 | # IDEs and editors
10 | /.idea
11 | .project
12 | .classpath
13 | .c9/
14 | *.launch
15 | .settings/
16 |
17 | # IDE - VSCode
18 | .vscode/
19 | !.vscode/settings.json
20 | !.vscode/tasks.json
21 | !.vscode/launch.json
22 | !.vscode/extensions.json
23 |
24 | # misc
25 | dist/qan-app-*.tar.gz
26 | /.sass-cache
27 | /connect.lock
28 | /coverage/*
29 | /libpeerconnection.log
30 | npm-debug.log
31 | testem.log
32 | /typings
33 |
34 | # e2e
35 | /e2e/*.js
36 | /e2e/*.map
37 |
38 | #System Files
39 | .DS_Store
40 | Thumbs.db
41 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | dist: trusty
2 |
3 | language: node_js
4 | node_js:
5 | - "10"
6 |
7 | # addons:
8 | # apt:
9 | # sources:
10 | # - google-chrome
11 | # packages:
12 | # - google-chrome-stable
13 |
14 | before_install:
15 | - sudo apt-get -yq --no-install-suggests --allow-unauthenticated --no-install-recommends $(travis_apt_get_options) install google-chrome-stable
16 |
17 | cache:
18 | directories:
19 | - ./node_modules
20 |
21 | install:
22 | - npm install
23 |
24 | script:
25 | # Use Chromium instead of Chrome.
26 | - export CHROME_BIN=google-chrome
27 | - export PATH=$(npm bin):$PATH
28 | - xvfb-run -a npm run lint
29 | # - xvfb-run -a npm run test -- --single-run --no-progress
30 | # - |
31 | # json-server --quiet --routes=routes.json db.js &
32 | # SERVER_PID=$!
33 | # - xvfb-run -a npm run e2e -- --proxy-config=proxy.conf.e2e.json --no-progress --config=protractor-ci.conf.js
34 | # - kill -9 $SERVER_PID
35 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | init:
2 | git config core.hooksPath .githooks
3 | npm ci
4 |
5 | build:
6 | rm -rf dist/*
7 | npm run build
8 | npm run pack
9 | deploy:
10 | docker exec pmm-server bash -c 'rm -rf /usr/share/percona-qan-app/*'
11 | docker cp dist/qan-app pmm-server:/opt/
12 | docker exec pmm-server bash -c 'mv /opt/qan-app/* /usr/share/percona-qan-app/'
13 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # QAN App
2 |
3 | [](https://www.codacy.com/app/Percona/qan-app?utm_source=github.com&utm_medium=referral&utm_content=percona/qan-app&utm_campaign=Badge_Grade)
4 | [](https://cla-assistant.percona.com/percona/qan-app)
5 |
6 | ## Development server
7 |
8 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
9 |
10 | ## Code scaffolding
11 |
12 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive/pipe/service/class/module`.
13 |
14 | ## Build
15 |
16 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build.
17 |
18 | ## Running unit tests
19 |
20 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
21 |
22 | ## Running end-to-end tests
23 |
24 | Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
25 | Before running the tests make sure you are serving the app via `ng serve`.
26 |
27 | ## Deploying to GitHub Pages
28 |
29 | Run `ng github-pages:deploy` to deploy to GitHub Pages.
30 |
31 | ## Further help
32 |
33 | To get more help on the `angular-cli` use `ng help` or go check out the [Angular-CLI README](https://github.com/angular/angular-cli/blob/master/README.md).
34 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "qan-app": {
7 | "root": "",
8 | "sourceRoot": "src",
9 | "projectType": "application",
10 | "architect": {
11 | "build": {
12 | "builder": "@angular-devkit/build-angular:browser",
13 | "options": {
14 | "outputPath": "dist",
15 | "index": "src/index.html",
16 | "main": "src/main.ts",
17 | "tsConfig": "src/tsconfig.app.json",
18 | "polyfills": "src/polyfills.ts",
19 | "assets": [
20 | "src/assets",
21 | "src/favicon.ico"
22 | ],
23 | "styles": [
24 | "node_modules/font-awesome/scss/font-awesome.scss",
25 | "node_modules/bootstrap/scss/bootstrap.scss",
26 | "node_modules/highlight.js/styles/github-gist.css",
27 | "src/styles.scss"
28 | ],
29 | "scripts": []
30 | },
31 | "configurations": {
32 | "dev": {
33 | "fileReplacements": [
34 | {
35 | "replace": "src/app/environment.ts",
36 | "with": "src/environments/environment.ts"
37 | }
38 | ]
39 | },
40 | "production": {
41 | "optimization": true,
42 | "outputHashing": "all",
43 | "sourceMap": false,
44 | "extractCss": true,
45 | "namedChunks": false,
46 | "aot": true,
47 | "extractLicenses": true,
48 | "vendorChunk": false,
49 | "buildOptimizer": true,
50 | "fileReplacements": [
51 | {
52 | "replace": "src/app/environment.ts",
53 | "with": "src/environments/environment.prod.ts"
54 | }
55 | ]
56 | },
57 | "demo": {
58 | "fileReplacements": [
59 | {
60 | "replace": "src/app/environment.ts",
61 | "with": "src/environments/environment.demo.ts"
62 | }
63 | ]
64 | },
65 | "navless": {
66 | "fileReplacements": [
67 | {
68 | "replace": "src/app/environment.ts",
69 | "with": "src/environments/environment.navless.ts"
70 | }
71 | ]
72 | }
73 | }
74 | },
75 | "serve": {
76 | "builder": "@angular-devkit/build-angular:dev-server",
77 | "options": {
78 | "browserTarget": "qan-app:build"
79 | },
80 | "configurations": {
81 | "production": {
82 | "browserTarget": "qan-app:build:production"
83 | }
84 | }
85 | },
86 | "extract-i18n": {
87 | "builder": "@angular-devkit/build-angular:extract-i18n",
88 | "options": {
89 | "browserTarget": "qan-app:build"
90 | }
91 | },
92 | "test": {
93 | "builder": "@angular-devkit/build-angular:karma",
94 | "options": {
95 | "main": "src/test.ts",
96 | "karmaConfig": "./karma.conf.js",
97 | "polyfills": "src/polyfills.ts",
98 | "tsConfig": "src/tsconfig.spec.json",
99 | "codeCoverage": true,
100 | "scripts": [],
101 | "styles": [
102 | "node_modules/font-awesome/scss/font-awesome.scss",
103 | "node_modules/bootstrap/scss/bootstrap.scss",
104 | "node_modules/highlight.js/styles/github-gist.css",
105 | "src/styles.scss"
106 | ],
107 | "assets": [
108 | "src/assets",
109 | "src/favicon.ico"
110 | ]
111 | }
112 | },
113 | "lint": {
114 | "builder": "@angular-devkit/build-angular:tslint",
115 | "options": {
116 | "tsConfig": [
117 | "src/tsconfig.app.json",
118 | "src/tsconfig.spec.json"
119 | ],
120 | "exclude": []
121 | }
122 | }
123 | }
124 | },
125 | "qan-app-e2e": {
126 | "root": "e2e",
127 | "sourceRoot": "e2e",
128 | "projectType": "application",
129 | "architect": {
130 | "e2e": {
131 | "builder": "@angular-devkit/build-angular:protractor",
132 | "options": {
133 | "protractorConfig": "./protractor.conf.js",
134 | "devServerTarget": "qan-app:serve"
135 | }
136 | },
137 | "lint": {
138 | "builder": "@angular-devkit/build-angular:tslint",
139 | "options": {
140 | "tsConfig": [
141 | "e2e/tsconfig.e2e.json"
142 | ],
143 | "exclude": []
144 | }
145 | }
146 | }
147 | }
148 | },
149 | "defaultProject": "qan-app",
150 | "schematics": {
151 | "@schematics/angular:component": {
152 | "inlineTemplate": false,
153 | "spec": true,
154 | "prefix": "app",
155 | "styleext": "css"
156 | },
157 | "@schematics/angular:directive": {
158 | "prefix": "app"
159 | }
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/db.js:
--------------------------------------------------------------------------------
1 | module.exports = function () {
2 | return {
3 | "instances": require('./jsons/qan-api/instances.json').instances,
4 | flight: require('./jsons/1.json')
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/dist/qan-app/assets/50747258.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/percona/qan-app/dc15e33d41a0b87abec08abb83aded5b0f83356a/dist/qan-app/assets/50747258.png
--------------------------------------------------------------------------------
/dist/qan-app/assets/Percona_XtraDB_Cluster.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/percona/qan-app/dc15e33d41a0b87abec08abb83aded5b0f83356a/dist/qan-app/assets/Percona_XtraDB_Cluster.png
--------------------------------------------------------------------------------
/dist/qan-app/assets/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/percona/qan-app/dc15e33d41a0b87abec08abb83aded5b0f83356a/dist/qan-app/assets/background.png
--------------------------------------------------------------------------------
/dist/qan-app/assets/database.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/percona/qan-app/dc15e33d41a0b87abec08abb83aded5b0f83356a/dist/qan-app/assets/database.png
--------------------------------------------------------------------------------
/dist/qan-app/assets/footer-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/percona/qan-app/dc15e33d41a0b87abec08abb83aded5b0f83356a/dist/qan-app/assets/footer-logo.png
--------------------------------------------------------------------------------
/dist/qan-app/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/percona/qan-app/dc15e33d41a0b87abec08abb83aded5b0f83356a/dist/qan-app/assets/logo.png
--------------------------------------------------------------------------------
/dist/qan-app/assets/mysql_64_black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/percona/qan-app/dc15e33d41a0b87abec08abb83aded5b0f83356a/dist/qan-app/assets/mysql_64_black.png
--------------------------------------------------------------------------------
/dist/qan-app/assets/percona-server-black-50.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/percona/qan-app/dc15e33d41a0b87abec08abb83aded5b0f83356a/dist/qan-app/assets/percona-server-black-50.png
--------------------------------------------------------------------------------
/dist/qan-app/assets/pmm-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
24 |
25 |
60 |
68 |
69 |
70 |
72 |
73 |
74 |
75 |
76 |
79 |
80 |
--------------------------------------------------------------------------------
/dist/qan-app/assets/sm-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/percona/qan-app/dc15e33d41a0b87abec08abb83aded5b0f83356a/dist/qan-app/assets/sm-logo.png
--------------------------------------------------------------------------------
/dist/qan-app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/percona/qan-app/dc15e33d41a0b87abec08abb83aded5b0f83356a/dist/qan-app/favicon.ico
--------------------------------------------------------------------------------
/dist/qan-app/fontawesome-webfont.674f50d287a8c48dc19b.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/percona/qan-app/dc15e33d41a0b87abec08abb83aded5b0f83356a/dist/qan-app/fontawesome-webfont.674f50d287a8c48dc19b.eot
--------------------------------------------------------------------------------
/dist/qan-app/fontawesome-webfont.af7ae505a9eed503f8b8.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/percona/qan-app/dc15e33d41a0b87abec08abb83aded5b0f83356a/dist/qan-app/fontawesome-webfont.af7ae505a9eed503f8b8.woff2
--------------------------------------------------------------------------------
/dist/qan-app/fontawesome-webfont.b06871f281fee6b241d6.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/percona/qan-app/dc15e33d41a0b87abec08abb83aded5b0f83356a/dist/qan-app/fontawesome-webfont.b06871f281fee6b241d6.ttf
--------------------------------------------------------------------------------
/dist/qan-app/fontawesome-webfont.fee66e712a8a08eef580.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/percona/qan-app/dc15e33d41a0b87abec08abb83aded5b0f83356a/dist/qan-app/fontawesome-webfont.fee66e712a8a08eef580.woff
--------------------------------------------------------------------------------
/dist/qan-app/footer-logo.f5f92e041aab6e0a0731.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/percona/qan-app/dc15e33d41a0b87abec08abb83aded5b0f83356a/dist/qan-app/footer-logo.f5f92e041aab6e0a0731.png
--------------------------------------------------------------------------------
/dist/qan-app/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Percona Query Analytics
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/dist/qan-app/pmm-logo.774290ea45f094ac7586.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
24 |
25 |
60 |
68 |
69 |
70 |
72 |
73 |
74 |
75 |
76 |
79 |
80 |
--------------------------------------------------------------------------------
/dist/qan-app/runtime.458556a34b891ea32398.js:
--------------------------------------------------------------------------------
1 | !function(e){function r(r){for(var n,l,f=r[0],i=r[1],p=r[2],c=0,s=[];c {
7 | page = new QanAppPage();
8 | });
9 |
10 | it('should display message saying app works', () => {
11 | page.navigateTo();
12 | expect(page.isMainPresent()).toEqual(true);
13 | });
14 | });
15 |
--------------------------------------------------------------------------------
/e2e/app.po.ts:
--------------------------------------------------------------------------------
1 | import { browser, element, by } from 'protractor';
2 |
3 | export class QanAppPage {
4 | navigateTo() {
5 | return browser.get('/');
6 | }
7 |
8 | getParagraphText() {
9 | return element(by.css('app-root')).getText();
10 | }
11 |
12 | isMainPresent() {
13 | return $('app-root').$('main').isPresent();
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/e2e/tsconfig.e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "sourceMap": true,
4 | "declaration": false,
5 | "moduleResolution": "node",
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "lib": [
9 | "es2016",
10 | "dom"
11 | ],
12 | "outDir": "../dist/out-tsc-e2e",
13 | "module": "commonjs",
14 | "target": "es6",
15 | "types":[
16 | "jasmine",
17 | "node"
18 | ]
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/jsons/1.json:
--------------------------------------------------------------------------------
1 | {
2 | "posts": [
3 | { "id": 1, "title": "json-server", "author": "typicode" }
4 | ],
5 | "comments": [
6 | { "id": 1, "body": "some comment", "postId": 1 }
7 | ],
8 | "profile": { "name": "typicode" }
9 | }
10 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | // Karma configuration file, see link for more information
2 | // https://karma-runner.github.io/0.13/config/configuration-file.html
3 |
4 | module.exports = function (config) {
5 | config.set({
6 | basePath: '',
7 | frameworks: ['jasmine', '@angular-devkit/build-angular'],
8 | plugins: [
9 | require('karma-jasmine'),
10 | require('karma-chrome-launcher'),
11 | require('karma-jasmine-html-reporter'),
12 | require('karma-coverage-istanbul-reporter'),
13 | require('@angular-devkit/build-angular/plugins/karma')
14 | ],
15 | browsers: ['ChromeNoSandbox'],
16 | customLaunchers: {
17 | ChromeNoSandbox: {
18 | base: 'Chrome',
19 | flags: ['--no-sandbox']
20 | }
21 | },
22 | client: {
23 | clearContext: false // leave Jasmine Spec Runner output visible in browser
24 | },
25 | files: [ ],
26 | preprocessors: { },
27 | mime: {
28 | 'text/x-typescript': ['ts', 'tsx']
29 | },
30 | coverageIstanbulReporter: {
31 | reports: [ 'html', 'lcovonly' ],
32 | fixWebpackSourcePaths: true,
33 | thresholds: {
34 | statements: 80,
35 | lines: 80,
36 | branches: 80,
37 | functions: 80
38 | }
39 | },
40 | reporters: config.angularCli && config.angularCli.codeCoverage ? ['progress', 'coverage-istanbul', 'kjhtml'] : ['progress', 'kjhtml'],
41 | port: 9876,
42 | colors: true,
43 | logLevel: config.LOG_INFO,
44 | autoWatch: true,
45 | singleRun: false,
46 | browserNoActivityTimeout: 100000
47 | });
48 | };
49 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "qan-app",
3 | "version": "v1.17.5",
4 | "license": "AGPL-3.0",
5 | "angular-cli": {},
6 | "scripts": {
7 | "ng": "PATH=$(npm bin):$PATH; ng",
8 | "start": "ng serve --proxy-config proxy.conf.json",
9 | "lint": "ng lint",
10 | "format": "tsfmt -r",
11 | "test": "ng test",
12 | "test:coverage-report": "open coverage/index.html",
13 | "e2e": "ng e2e --proxy-config proxy.conf.json",
14 | "server": "PATH=$(npm bin):$PATH; webpack-dev-server --config webpack.config.js",
15 | "build": "ng build qan-app --base-href '/qan/' --aot --output-path 'dist/qan-app' --prod --progress --output-hashing=all",
16 | "pack": "cd dist/; tar -czf qan-app-`date +%Y%m%dT%H%m%S`.tar.gz qan-app",
17 | "clean": "rm -r dist/*"
18 | },
19 | "private": true,
20 | "dependencies": {
21 | "@angular/common": "^8.1.1",
22 | "@angular/compiler": "^8.1.1",
23 | "@angular/core": "^8.1.1",
24 | "@angular/forms": "^8.1.1",
25 | "@angular/material": "^8.0.2",
26 | "@angular/platform-browser": "^8.1.1",
27 | "@angular/platform-browser-dynamic": "^8.1.1",
28 | "@angular/router": "^8.1.1",
29 | "@ng-bootstrap/ng-bootstrap": "^4.2.1",
30 | "@ng-select/ng-select": "^2.20.5",
31 | "@types/d3-shape": "^1.3.1",
32 | "@types/jszip": "^3.1.6",
33 | "bootstrap": "^4.3.1",
34 | "core-js": "^2.6.9",
35 | "d3": "^5.9.7",
36 | "font-awesome": "4.7.0",
37 | "highlight.js": "10.4.1",
38 | "hoek": "^5.0.4",
39 | "jszip": "^3.2.2",
40 | "moment": "^2.24.0",
41 | "ngx-clipboard": "^12.2.0",
42 | "ngx-pagination": "^3.3.1",
43 | "ngx-perfect-scrollbar": "^7.2.1",
44 | "numeral": "2.0.6",
45 | "perfect-scrollbar": "^1.4.0",
46 | "renderjson": "github:caldwell/renderjson",
47 | "rxjs": "^6.5.2",
48 | "stream": "0.0.2",
49 | "ts-helpers": "1.1.2",
50 | "tslib": "^1.10.0",
51 | "vkbeautify": "^0.99.3",
52 | "zone.js": "~0.9.1",
53 | "lodash.template": ">=4.5.0",
54 | "serialize-javascript": ">=2.1.1"
55 | },
56 | "devDependencies": {
57 | "@angular-devkit/build-angular": "~0.803.27",
58 | "@angular/cli": "^8.1.1",
59 | "@angular/compiler-cli": "^8.1.1",
60 | "@types/d3": "5.0.0",
61 | "@types/highlight.js": "9.12.3",
62 | "@types/jasmine": "2.8.8",
63 | "@types/node": "^10.14.12",
64 | "@types/numeral": "0.0.25",
65 | "@types/vkbeautify": "0.99.2",
66 | "acorn": "6.4.1",
67 | "codelyzer": "^5.0.1",
68 | "handlebars": "^4.7.6",
69 | "jasmine": "^3.4.0",
70 | "jasmine-core": "^3.4.0",
71 | "jasmine-spec-reporter": "4.2.1",
72 | "json-server": "^0.14.2",
73 | "karma": "^3.1.4",
74 | "karma-chrome-launcher": "2.2.0",
75 | "karma-cli": "1.0.1",
76 | "karma-coverage-istanbul-reporter": "^2.0.5",
77 | "karma-jasmine": "1.1.2",
78 | "karma-jasmine-html-reporter": "^1.4.2",
79 | "karma-remap-istanbul": "0.6.0",
80 | "kind-of": "^6.0.3",
81 | "minimist": "^1.2.5",
82 | "ng-swagger-gen": "^1.7.1",
83 | "protractor": "5.4.0",
84 | "raw-loader": "^0.5.1",
85 | "readable-stream": "^2.3.6",
86 | "sass-loader": "^7.1.0",
87 | "ts-node": "^7.0.1",
88 | "tslint": "5.11.0",
89 | "typescript": "3.4.5",
90 | "typescript-formatter": "^7.2.2"
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/protractor-ci.conf.js:
--------------------------------------------------------------------------------
1 | const config = require('./protractor.conf').config;
2 |
3 | config.capabilities = {
4 | browserName: 'chrome',
5 | chromeOptions: {
6 | args: ['--no-sandbox']
7 | }
8 | };
9 |
10 | exports.config = config;
11 |
--------------------------------------------------------------------------------
/protractor.conf.js:
--------------------------------------------------------------------------------
1 | // Protractor configuration file, see link for more information
2 | // https://github.com/angular/protractor/blob/master/lib/config.ts
3 |
4 | /*global jasmine */
5 | const { SpecReporter } = require('jasmine-spec-reporter');
6 |
7 | exports.config = {
8 | allScriptsTimeout: 11000,
9 | specs: [
10 | './e2e/**/*.e2e-spec.ts'
11 | ],
12 | capabilities: {
13 | 'browserName': 'chrome'
14 | },
15 | directConnect: true,
16 | baseUrl: 'http://localhost:4200/',
17 | framework: 'jasmine',
18 | jasmineNodeOpts: {
19 | showColors: true,
20 | defaultTimeoutInterval: 30000,
21 | print: function() {}
22 | },
23 | beforeLaunch: function() {
24 | require('ts-node').register({
25 | project: 'e2e/tsconfig.e2e.json'
26 | });
27 | },
28 | onPrepare: function() {
29 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/proxy.conf.e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "/qan-api": {
3 | "target": "http://localhost:3000",
4 | "secure": false,
5 | "logLevel": "debug"
6 | },
7 | "/managed": {
8 | "target": "http://localhost",
9 | "secure": false,
10 | "logLevel": "debug"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/proxy.conf.json:
--------------------------------------------------------------------------------
1 | {
2 | "/qan-api": {
3 | "target": "http://localhost",
4 | "secure": false,
5 | "logLevel": "debug"
6 | },
7 | "/managed": {
8 | "target": "http://localhost",
9 | "secure": false,
10 | "logLevel": "debug"
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/routes.json:
--------------------------------------------------------------------------------
1 | {
2 | "/qan-api/*": "/$1"
3 | }
4 |
--------------------------------------------------------------------------------
/src/app/add-amazon-rds/add-amazon-rds.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
Amazon RDS Credentials
4 |
5 |
24 |
27 |
32 |
33 |
Amazon RDS Instances
34 |
35 |
36 |
37 |
38 |
39 | {{ errorMessage }}
40 |
41 |
42 |
92 |
--------------------------------------------------------------------------------
/src/app/add-amazon-rds/add-amazon-rds.component.scss:
--------------------------------------------------------------------------------
1 | .aws-table {
2 | width: 100%;
3 | }
4 |
5 | .not-checked-button {
6 | padding: .375rem .75rem;
7 | }
8 |
9 | .not-checked-button:not(:last-child) {
10 | margin-right: 20px;
11 | }
12 |
--------------------------------------------------------------------------------
/src/app/add-amazon-rds/add-amazon-rds.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { AddAmazonRDSComponent } from './add-amazon-rds.component';
4 |
5 | describe('AddAmazonRDSComponent', () => {
6 | let component: AddAmazonRDSComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ AddAmazonRDSComponent ]
12 | })
13 | .compileComponents();
14 | }));
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(AddAmazonRDSComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should create', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/app/add-amazon-rds/add-amazon-rds.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { RDSCredentials, MySQLCredentials, RDSInstance, RDSNode, AddAmazonRDSService } from './add-amazon-rds.service'
3 | import { environment } from '../environment';
4 |
5 | @Component({
6 | selector: 'app-add-aws',
7 | templateUrl: './add-amazon-rds.component.html',
8 | styleUrls: ['./add-amazon-rds.component.scss']
9 | })
10 | export class AddAmazonRDSComponent implements OnInit {
11 |
12 | rdsCredentials = new RDSCredentials();
13 | mysqlCredentials = new MySQLCredentials();
14 | rdsNode = {} as RDSNode;
15 | allRDSInstances: RDSInstance[] = [];
16 | registeredRDSInstances: RDSInstance[] = [];
17 | registeredNames: string[] = [];
18 | isLoading: boolean;
19 | errorMessage: string;
20 | isDemo = false;
21 | submitted = false;
22 |
23 | constructor(public addAmazonRDSService: AddAmazonRDSService) {
24 | this.isDemo = environment.demoHosts.includes(location.hostname);
25 | }
26 |
27 | async onSubmit() {
28 | this.errorMessage = '';
29 | this.isLoading = true;
30 | this.submitted = true;
31 | try {
32 | this.allRDSInstances = await this.addAmazonRDSService.discover(this.rdsCredentials);
33 | await this.getRegistered();
34 | this.errorMessage = '';
35 | } catch (err) {
36 | this.allRDSInstances = [];
37 | let msg = err.json().error;
38 | if (msg.startsWith('NoCredentialProviders')) {
39 | msg = 'Cannot discover instances - please provide Amazon RDS access credentials';
40 | }
41 | this.errorMessage = msg;
42 | } finally {
43 | this.isLoading = false;
44 | }
45 | }
46 |
47 | enableInstanceMonitoring(node: RDSNode) {
48 | this.mysqlCredentials = new MySQLCredentials();
49 | this.rdsNode = { name: node.name, region: node.region } as RDSNode;
50 | }
51 |
52 | showConnect(node: RDSNode): boolean {
53 | return this.rdsNode.name === node.name && this.rdsNode.region === node.region;
54 | }
55 |
56 | cancel() {
57 | this.rdsNode.name = '';
58 | this.rdsNode.region = '';
59 | }
60 |
61 | async onConnect() {
62 | this.errorMessage = '';
63 | try {
64 | const res = await this.addAmazonRDSService.enable(this.rdsCredentials, this.rdsNode, this.mysqlCredentials);
65 | } catch (err) {
66 | this.errorMessage = err.json().error;
67 | return;
68 | }
69 | this.rdsNode = {} as RDSNode;
70 | this.cancel();
71 | await this.getRegistered();
72 | }
73 |
74 | async disableInstanceMonitoring(node: RDSNode) {
75 | if (this.isDemo) {
76 | return false;
77 | }
78 | this.errorMessage = '';
79 | const text = `Are you sure want to disable monitoring of '${node.name}:${node.region}' node?`;
80 | if (confirm(text)) {
81 | try {
82 | const res = await this.addAmazonRDSService.disable(node);
83 | await this.getRegistered();
84 | } catch (err) {
85 | this.errorMessage = err.json().error;
86 | }
87 | }
88 | }
89 |
90 | isEnabled(rdsInstance: RDSInstance): boolean {
91 | return this.registeredNames.indexOf(rdsInstance.node.name + ':' + rdsInstance.node.region) > -1;
92 | }
93 |
94 | async getRegistered() {
95 | this.errorMessage = '';
96 | try {
97 | this.registeredRDSInstances = await this.addAmazonRDSService.getRegistered();
98 | } catch (err) {
99 | this.errorMessage = err.json().error;
100 | }
101 | this.registeredNames = [];
102 | if (this.registeredRDSInstances !== undefined) {
103 | this.registeredRDSInstances.forEach(element => {
104 | this.registeredNames.push(element.node.name + ':' + element.node.region);
105 | });
106 | }
107 | }
108 |
109 | async ngOnInit() {
110 | this.errorMessage = '';
111 | try {
112 | const allRDSInstances = await this.addAmazonRDSService.discover(this.rdsCredentials);
113 | if (this.submitted) { // ignore results if user submitted form with creds.
114 | return;
115 | }
116 | await this.getRegistered();
117 | this.allRDSInstances = allRDSInstances;
118 | this.errorMessage = '';
119 |
120 | } catch (err) {
121 | if (this.submitted) { // ignore results if user submitted form with creds.
122 | return;
123 | }
124 | let msg = err.json().error;
125 | if (msg.startsWith('NoCredentialProviders')) {
126 | msg = 'Cannot automatically discover instances - please provide Amazon RDS access credentials';
127 | }
128 | this.errorMessage = msg;
129 | }
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/src/app/add-amazon-rds/add-amazon-rds.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed, inject } from '@angular/core/testing';
2 |
3 | import { AddAmazonRDSService } from './add-amazon-rds.service';
4 |
5 | describe('AddAmazonRDSService', () => {
6 | beforeEach(() => {
7 | TestBed.configureTestingModule({
8 | providers: [AddAmazonRDSService]
9 | });
10 | });
11 |
12 | it('should be created', inject([AddAmazonRDSService], (service: AddAmazonRDSService) => {
13 | expect(service).toBeTruthy();
14 | }));
15 | });
16 |
--------------------------------------------------------------------------------
/src/app/add-amazon-rds/add-amazon-rds.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core';
2 | import {HttpClient, HttpHeaders} from '@angular/common/http';
3 |
4 | export class RDSCredentials {
5 | constructor(public aws_access_key_id = '', public aws_secret_access_key = '') {
6 | }
7 | }
8 |
9 | export class MySQLCredentials {
10 | constructor(public username = '', public password = '') {
11 | }
12 | }
13 |
14 | export interface RDSNode {
15 | name: string;
16 | region: string;
17 | }
18 |
19 | export interface RDSService {
20 | address: string;
21 | port: number;
22 | engine: string;
23 | engine_version: string;
24 | }
25 |
26 | export interface RDSInstance {
27 | node: RDSNode;
28 | service: RDSService;
29 | }
30 |
31 | @Injectable()
32 | export class AddAmazonRDSService {
33 |
34 | private headers = new HttpHeaders({'Content-Type': 'application/json'});
35 |
36 | constructor(private httpClient: HttpClient) {
37 | }
38 |
39 | async discover(rdsCredentials: RDSCredentials): Promise {
40 | const url = `/managed/v0/rds/discover`;
41 | const data = {
42 | aws_access_key_id: rdsCredentials.aws_access_key_id,
43 | aws_secret_access_key: rdsCredentials.aws_secret_access_key
44 | };
45 | const response = await this.httpClient
46 | .post(url, data, {headers: this.headers})
47 | .toPromise();
48 | return response['instances'] as RDSInstance[];
49 | }
50 |
51 | async enable(rdsCredentials: RDSCredentials, node: RDSNode, mysqlCredentials: MySQLCredentials): Promise<{}> {
52 | const url = `/managed/v0/rds`;
53 | const data = {
54 | aws_access_key_id: rdsCredentials.aws_access_key_id,
55 | aws_secret_access_key: rdsCredentials.aws_secret_access_key,
56 | id: {name: node.name, region: node.region},
57 | password: mysqlCredentials.password,
58 | username: mysqlCredentials.username
59 | };
60 | return await this.httpClient
61 | .post(url, data, {headers: this.headers})
62 | .toPromise();
63 | }
64 |
65 | async disable(node: RDSNode): Promise<{}> {
66 | const url = `/managed/v0/rds`;
67 | const body = {id: {name: node.name, region: node.region}};
68 | return await this.httpClient.request('delete', url, {body: body})
69 | .toPromise();
70 | }
71 |
72 | async getRegistered(): Promise {
73 | const url = `/managed/v0/rds`;
74 | const response = await this.httpClient
75 | .get(url, {headers: this.headers})
76 | .toPromise();
77 | return response['instances'] as RDSInstance[];
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/app/add-instance/add-instance.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
How to Add an Instance
4 |
5 |
6 |
46 |
--------------------------------------------------------------------------------
/src/app/add-instance/add-instance.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/percona/qan-app/dc15e33d41a0b87abec08abb83aded5b0f83356a/src/app/add-instance/add-instance.component.scss
--------------------------------------------------------------------------------
/src/app/add-instance/add-instance.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { AddInstanceComponent } from './add-instance.component';
4 |
5 | describe('AddInstanceComponent', () => {
6 | let component: AddInstanceComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ AddInstanceComponent ]
12 | })
13 | .compileComponents();
14 | }));
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(AddInstanceComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should create', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/app/add-instance/add-instance.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-add-instance',
5 | templateUrl: './add-instance.component.html',
6 | styleUrls: ['./add-instance.component.scss']
7 | })
8 | export class AddInstanceComponent implements OnInit {
9 |
10 | constructor() { }
11 |
12 | ngOnInit() {
13 | }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/src/app/add-remote-instances/add-remote-instance.component.html:
--------------------------------------------------------------------------------
1 |
113 |
--------------------------------------------------------------------------------
/src/app/add-remote-instances/add-remote-instance.component.scss:
--------------------------------------------------------------------------------
1 | .add-remote-instance {
2 | .header p {
3 | font-size: 1.3rem;
4 | color: var(--white);
5 | }
6 |
7 | .tab-pane {
8 | background-color: #1f1d1d
9 | }
10 |
11 | .input-wrapper {
12 | min-height: 75px;
13 | }
14 |
15 | input.custom-form-input {
16 | height: 38px;
17 | color: var(--white);
18 | font-weight: 300;
19 | border-radius: 0;
20 | border: 1px solid #2a2a2a;
21 | background-color: #141414 !important;
22 | }
23 |
24 | .custom-form-input--not-valid {
25 | border-color: red !important;
26 | }
27 |
28 | form.content {
29 | padding: 40px 0;
30 | }
31 |
32 | input[type=text] {
33 | margin-bottom: 5px;
34 | border-radius: 3px;
35 | }
36 |
37 | input::placeholder {
38 | font-style: italic;
39 | }
40 |
41 | .not-checked-button {
42 | padding: .375rem .75rem;
43 | cursor: pointer;
44 | }
45 |
46 | .not-checked-button + div {
47 | margin-left: 20px;
48 | }
49 |
50 | .fa-info-circle {
51 | font-size: 20px;
52 | }
53 |
54 | .app-tooltip-wrapper {
55 | position: relative;
56 | }
57 |
58 | div.app-tooltip--remote-instance {
59 | cursor: pointer;
60 | }
61 |
62 | div.app-tooltip--remote-instance::before {
63 | content: 'Information';
64 | left: 25px;
65 | top: -3px;
66 | padding: 5px;
67 | }
68 |
69 | div.app-tooltip--hostname::before {
70 | content: 'Public DNS hostname of your instance';
71 | }
72 |
73 | div.app-tooltip--name::before {
74 | content: 'Instance name to use';
75 | }
76 |
77 | div.app-tooltip--port::before {
78 | content: 'Port your instance is listening on';
79 | }
80 |
81 | div.app-tooltip--username::before {
82 | content: 'Your database user name';
83 | }
84 |
85 | div.app-tooltip--password::before {
86 | content: 'Your database password';
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/app/add-remote-instances/add-remote-instance.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, OnInit} from '@angular/core';
2 | import {AddRemoteInstanceService, RemoteInstanceCredentials} from './add-remote-instance.service'
3 | import {environment} from '../environment';
4 | import {Router} from '@angular/router';
5 |
6 | @Component({
7 | selector: 'app-add-remote-postgres',
8 | templateUrl: './add-remote-instance.component.html',
9 | styleUrls: ['./add-remote-instance.component.scss']
10 | })
11 | export class AddRemoteInstanceComponent implements OnInit {
12 |
13 | remoteInstanceCredentials = {} as RemoteInstanceCredentials;
14 | errorMessage: string;
15 | isDemo = false;
16 | isLoading = false;
17 | isSubmitted = false;
18 | instanceType: string;
19 | currentUrl: string;
20 |
21 | constructor(public addRemoteInstanceService: AddRemoteInstanceService, private router: Router) {
22 | this.isDemo = environment.demoHosts.includes(location.hostname);
23 | this.currentUrl = this.router.url;
24 | }
25 |
26 | async ngOnInit() {
27 | this.errorMessage = '';
28 | this.isLoading = false;
29 | this.instanceType =
30 | this.addRemoteInstanceService.checkInstanceType(this.currentUrl) === 'postgresql' ? 'PostgreSQL' : 'MySQL';
31 | }
32 |
33 | async onSubmit(form) {
34 | const currentUrl = `${window.parent.location}`;
35 | const newURL = currentUrl.split('/graph/d/').shift() + '/graph/d/pmm-list/';
36 |
37 | this.errorMessage = '';
38 | this.isSubmitted = true;
39 | if (!form.valid) { return; }
40 | this.isLoading = true;
41 |
42 | if (this.remoteInstanceCredentials.name === undefined || this.remoteInstanceCredentials.name === '') {
43 | this.remoteInstanceCredentials.name = this.remoteInstanceCredentials.address; // set default value for name (like address)
44 | }
45 |
46 | if (this.remoteInstanceCredentials.port === undefined || this.remoteInstanceCredentials.port === '') {
47 | this.remoteInstanceCredentials.port = this.instanceType === 'PostgreSQL' ? '5432' : '3306'; // set default value for port
48 | }
49 |
50 | try {
51 | const res = await this.addRemoteInstanceService.enable(this.remoteInstanceCredentials, this.currentUrl)
52 | .then(() => {
53 | window.parent.location.assign(newURL);
54 | });
55 |
56 | } catch (err) {
57 | this.errorMessage = err.json().error;
58 | }
59 | this.isLoading = false;
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/src/app/add-remote-instances/add-remote-instance.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core';
2 | import {HttpClient, HttpHeaders} from '@angular/common/http';
3 |
4 | export interface RemoteInstanceCredentials {
5 | address: string;
6 | name: string;
7 | port: string;
8 | username: string;
9 | password: string;
10 | }
11 |
12 | export interface RemoteInstanceNode {
13 | id: number
14 | name: string
15 | region: string
16 | }
17 |
18 | export interface RemoteInstanceService {
19 | type: string;
20 | address: string;
21 | port: number;
22 | engine: string;
23 | engine_version: string;
24 | }
25 |
26 | export interface RemoteInstance {
27 | node: RemoteInstanceNode;
28 | service: RemoteInstanceService;
29 | }
30 |
31 | @Injectable()
32 | export class AddRemoteInstanceService {
33 |
34 | private headers = new HttpHeaders({'Content-Type': 'application/json'});
35 | instanceUrlPart: string;
36 |
37 | constructor(private http: HttpClient) {
38 | }
39 |
40 | async enable(remoteInstanceCredentials: RemoteInstanceCredentials, currentUrl): Promise<{}> {
41 | this.instanceUrlPart = this.checkInstanceType(currentUrl);
42 |
43 | const url = `/managed/v0/${this.instanceUrlPart}`;
44 | const data = {
45 | address: remoteInstanceCredentials.address,
46 | name: remoteInstanceCredentials.name,
47 | port: remoteInstanceCredentials.port,
48 | password: remoteInstanceCredentials.password,
49 | username: remoteInstanceCredentials.username
50 | };
51 | return await this.http
52 | .post(url, data, {headers: this.headers})
53 | .toPromise();
54 | }
55 |
56 | /**
57 | * Returns type of remote instance
58 | * @param currentUrl current page url
59 | */
60 | checkInstanceType(currentUrl) {
61 | return currentUrl === '/add-remote-postgres' ? 'postgresql' : 'mysql';
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/app/app-routing.module.ts:
--------------------------------------------------------------------------------
1 | import { NgModule, Injectable } from '@angular/core';
2 | import { Routes, Router, RouterModule, CanActivate } from '@angular/router';
3 | import { PageNotFoundComponent } from './core/page-not-found/page-not-found.component';
4 |
5 | import { QueryProfileComponent } from './query-profile/query-profile.component';
6 | import { MySQLQueryDetailsComponent } from './mysql-query-details/mysql-query-details.component';
7 | import { MongoQueryDetailsComponent } from './mongo-query-details/mongo-query-details.component';
8 | import { SummaryComponent } from './summary/summary.component';
9 | import { SettingsComponent } from './settings/settings.component';
10 | import { AddInstanceComponent } from './add-instance/add-instance.component';
11 | import { AddAmazonRDSComponent } from './add-amazon-rds/add-amazon-rds.component';
12 | import { InstanceService } from './core/instance.service';
13 | import { AddRemoteInstanceComponent } from './add-remote-instances/add-remote-instance.component';
14 | import {RemoteInstancesListComponent} from './remote-instances-list/remote-instances-list.component';
15 |
16 | @Injectable()
17 | export class RegisteredInstanceGuard implements CanActivate {
18 |
19 | private existsRegisteredInstances: boolean;
20 |
21 | constructor(public instanceService: InstanceService, public router: Router) {
22 | console.log('EmptyInstanceGuard.constructor', instanceService.dbServers);
23 | this.existsRegisteredInstances = instanceService.dbServers.length > 0;
24 | }
25 | canActivate() {
26 | if (!this.existsRegisteredInstances) {
27 | this.router.navigate(['add-instance']);
28 | }
29 | return this.existsRegisteredInstances;
30 | }
31 | }
32 |
33 | const routes: Routes = [
34 | { path: '', redirectTo: 'profile', pathMatch: 'full', canActivate: [RegisteredInstanceGuard] },
35 | {
36 | path: 'profile', component: QueryProfileComponent, canActivate: [RegisteredInstanceGuard], children: [
37 | { path: 'report/mysql', component: MySQLQueryDetailsComponent, canActivate: [RegisteredInstanceGuard] },
38 | { path: 'report/mongo', component: MongoQueryDetailsComponent, canActivate: [RegisteredInstanceGuard] }
39 | ]
40 | },
41 | { path: 'sys-summary', component: SummaryComponent, pathMatch: 'full', canActivate: [RegisteredInstanceGuard] },
42 | { path: 'settings', component: SettingsComponent, pathMatch: 'full', canActivate: [RegisteredInstanceGuard] },
43 | { path: 'add-instance', component: AddInstanceComponent, pathMatch: 'full' },
44 | { path: 'add-amazon-rds', component: AddAmazonRDSComponent, pathMatch: 'full' },
45 | { path: 'add-remote-postgres', component: AddRemoteInstanceComponent, pathMatch: 'full' },
46 | { path: 'add-remote-mysql', component: AddRemoteInstanceComponent, pathMatch: 'full' },
47 | { path: 'pmm-list', component: RemoteInstancesListComponent, pathMatch: 'full' },
48 | { path: '**', component: PageNotFoundComponent }
49 | ];
50 |
51 | @NgModule({
52 | imports: [RouterModule.forRoot(routes)],
53 | providers: [RegisteredInstanceGuard],
54 | exports: [RouterModule]
55 | })
56 | export class AppRoutingModule { }
57 |
--------------------------------------------------------------------------------
/src/app/app.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | {{ version }}
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/app/app.component.scss:
--------------------------------------------------------------------------------
1 | @media printer {
2 | footer {
3 | display: none;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/app/app.component.spec.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable:no-unused-variable */
2 |
3 | import { TestBed, async } from '@angular/core/testing';
4 | import { AppComponent } from './app.component';
5 | import { AppRoutingModule } from './app-routing.module';
6 | import { RouterTestingModule } from '@angular/router/testing';
7 |
8 | describe('AppComponent', () => {
9 | beforeEach(() => {
10 | TestBed.configureTestingModule({
11 | imports: [ RouterTestingModule, AppRoutingModule ],
12 | declarations: [
13 | AppComponent
14 | ],
15 | });
16 | TestBed.compileComponents();
17 | });
18 |
19 | it('should create the app', async(() => {
20 | const fixture = TestBed.createComponent(AppComponent);
21 | const app = fixture.debugElement.componentInstance;
22 | expect(app).toBeTruthy();
23 | }));
24 |
25 | it(`should have as title 'app works!'`, async(() => {
26 | const fixture = TestBed.createComponent(AppComponent);
27 | const app = fixture.debugElement.componentInstance;
28 | expect(app.title).toEqual('app works!');
29 | }));
30 |
31 | it('should render title in a h1 tag', async(() => {
32 | const fixture = TestBed.createComponent(AppComponent);
33 | fixture.detectChanges();
34 | const compiled = fixture.debugElement.nativeElement;
35 | expect(compiled.querySelector('h1').textContent).toContain('app works!');
36 | }));
37 | });
38 |
--------------------------------------------------------------------------------
/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, Inject, OnInit } from '@angular/core';
2 |
3 | import { InstanceService } from './core/instance.service';
4 | import { environment } from './environment';
5 | import * as moment from 'moment';
6 | import {DOCUMENT} from '@angular/common';
7 |
8 | @Component({
9 | selector: 'app-root',
10 | templateUrl: './app.component.html',
11 | styleUrls: ['./app.component.scss']
12 | })
13 | export class AppComponent implements OnInit {
14 | title = 'Query Analytics';
15 | version = environment.version;
16 | isDemo = false;
17 | hideNav = false;
18 | isInstancesListEmpty: boolean;
19 | theme = 'app-theme-light';
20 |
21 | constructor(instanceService: InstanceService, @Inject(DOCUMENT) private document) {
22 | this.isDemo = environment.demoHosts.indexOf(location.hostname) > -1;
23 | // show message how to configure pmm-client.
24 | this.hideNav = this.inIframe() || instanceService.dbServers.length === 0;
25 | }
26 |
27 | /**
28 | * inIframe is used to show/hide navbar.
29 | */
30 | inIframe(): boolean {
31 | let inIframe = false;
32 | try {
33 | inIframe = window.self !== window.top;
34 | } catch (e) {
35 | inIframe = true;
36 | }
37 | return inIframe;
38 | }
39 |
40 | getCookie(name) {
41 | return document.cookie.split('; ').reduce((r, v) => {
42 | const parts = v.split('=');
43 | return parts[0] === name ? decodeURIComponent(parts[1]) : r;
44 | }, '');
45 | }
46 |
47 | setCookie(key, value) {
48 | const expireDays = moment().utc().add(7, 'y').toString();
49 | document.cookie = `${key}=${value}; expires=${expireDays}; path=/`;
50 | }
51 |
52 | ngOnInit() {
53 | let res: any;
54 | res = this.getJsonFromUrl()
55 | const theme = res.theme || '';
56 | if (theme === '') {
57 | this.theme = this.getCookie('theme');
58 | } else if (theme === 'dark') {
59 | this.theme = 'app-theme-dark';
60 | } else if (theme === 'light') {
61 | this.theme = 'app-theme-light';
62 | }
63 | this.setCookie('theme', this.theme);
64 | this.document.body.className = this.theme;
65 | }
66 |
67 | getJsonFromUrl() {
68 | const query = location.search.substr(1);
69 | const result = {};
70 | query.split('&').forEach(function(part) {
71 | const item = part.split('=');
72 | result[item[0]] = decodeURIComponent(item[1]);
73 | });
74 | return result;
75 | }
76 |
77 | }
78 |
--------------------------------------------------------------------------------
/src/app/app.module.ts:
--------------------------------------------------------------------------------
1 | import { BrowserModule } from '@angular/platform-browser';
2 | import { NgModule, APP_INITIALIZER } from '@angular/core';
3 | import { FormsModule } from '@angular/forms';
4 | import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
5 |
6 | import { AppComponent } from './app.component';
7 | import { AppRoutingModule } from './app-routing.module';
8 | import { CoreModule } from './core/core.module';
9 | import { SharedModule } from './shared/shared.module';
10 |
11 | import { InstanceService } from './core/instance.service';
12 | import { AddAmazonRDSComponent } from './add-amazon-rds/add-amazon-rds.component';
13 | import { AddRemoteInstanceComponent } from './add-remote-instances/add-remote-instance.component';
14 | import { AddInstanceComponent } from './add-instance/add-instance.component';
15 | import {HttpClientModule} from '@angular/common/http';
16 | export function getInstances(instanceService: InstanceService) {
17 | return function () { return instanceService.getDBServers(); };
18 | }
19 |
20 | @NgModule({
21 | declarations: [
22 | AppComponent,
23 | AddAmazonRDSComponent,
24 | AddRemoteInstanceComponent,
25 | AddInstanceComponent,
26 | ],
27 | imports: [
28 | AppRoutingModule,
29 | BrowserModule,
30 | CoreModule,
31 | FormsModule,
32 | HttpClientModule,
33 | NgbModule.forRoot(),
34 | SharedModule,
35 | ],
36 | providers: [
37 | InstanceService,
38 | {
39 | provide: APP_INITIALIZER,
40 | useFactory: getInstances,
41 | deps: [InstanceService],
42 | multi: true
43 | }
44 | ],
45 | bootstrap: [AppComponent]
46 | })
47 | export class AppModule { }
48 |
--------------------------------------------------------------------------------
/src/app/core/core.component.ts:
--------------------------------------------------------------------------------
1 | import {OnDestroy} from '@angular/core';
2 | import {ParseQueryParamDatePipe} from '../shared/parse-query-param-date.pipe';
3 | import {Event, Router, ActivatedRoute, NavigationEnd} from '@angular/router';
4 | import * as moment from 'moment';
5 |
6 | import {environment} from '../environment';
7 | import {Subscription} from 'rxjs';
8 | import {Instance, InstanceService} from './instance.service';
9 | import {filter} from 'rxjs/operators';
10 |
11 | export interface QueryParams {
12 | from?: string;
13 | to?: string;
14 | 'var-host'?: string; // | string[];
15 | search?: string;
16 | queryID?: string;
17 | tz?: string;
18 | theme?: string;
19 | first_seen?: boolean;
20 | }
21 |
22 | /**
23 | * Base class for all components.
24 | */
25 | export abstract class CoreComponent implements OnDestroy {
26 |
27 | public isDemo = false;
28 | protected routerSubscription: Subscription;
29 | public queryParams: QueryParams;
30 | public previousQueryParams: QueryParams;
31 | public agent: Instance | null;
32 | public dbServer: Instance | null;
33 | public dbServers: Array = [];
34 | public dbServerMap: { [key: string]: Instance } = {};
35 | public from: any;
36 | public to: any;
37 | public isAllSelected: boolean;
38 | public isNotExistSelected: boolean;
39 | public isQueryDataAbsent: boolean;
40 |
41 | public fromUTCDate: string;
42 | public toUTCDate: string;
43 | parseQueryParamDatePipe = new ParseQueryParamDatePipe();
44 |
45 | constructor(protected route: ActivatedRoute, protected router: Router,
46 | protected instanceService: InstanceService) {
47 | this.isDemo = environment.demoHosts.includes(location.hostname);
48 | this.dbServer = instanceService.dbServers[0];
49 | this.agent = instanceService.dbServers[0].Agent;
50 | this.dbServers = instanceService.dbServers;
51 | this.dbServerMap = instanceService.dbServerMap;
52 | this.subscribeToRouter();
53 | }
54 |
55 | /**
56 | * Extract and convert query parameters.
57 | * Trigger onChangeParams method (must be overridden) on route change.
58 | */
59 | subscribeToRouter() {
60 |
61 | this.routerSubscription = this.router.events.pipe(
62 | filter((e: any) => e instanceof NavigationEnd)
63 | )
64 | .subscribe((event: Event) => {
65 | this.queryParams = this.route.snapshot.queryParams as QueryParams;
66 | this.parseParams();
67 |
68 | // trigger overriden method in child component
69 | this.onChangeParams(this.queryParams);
70 |
71 | this.previousQueryParams = Object.assign({}, this.queryParams);
72 | });
73 | }
74 |
75 | parseParams() {
76 | this.isAllSelected = this.queryParams['var-host'] === 'All';
77 | this.isQueryDataAbsent = (this.dbServer === null) && (!this.isAllSelected) && (!this.isNotExistSelected);
78 | try {
79 | this.dbServer = this.dbServerMap[this.queryParams['var-host']];
80 | this.agent = this.dbServerMap[this.queryParams['var-host']].Agent;
81 | } catch (err) {
82 | if (this.queryParams.hasOwnProperty('var-host')) {
83 | this.dbServer = null;
84 | this.agent = null;
85 | this.isNotExistSelected = !this.isAllSelected;
86 | } else {
87 | this.dbServer = this.instanceService.dbServers[0];
88 | this.agent = this.instanceService.dbServers[0].Agent;
89 | }
90 | }
91 | this.setTimeZoneFromParams();
92 | this.setThemeFromParams();
93 | this.from = this.parseQueryParamDatePipe.transform(this.queryParams.from, 'from');
94 | this.to = this.parseQueryParamDatePipe.transform(this.queryParams.to, 'to');
95 | this.fromUTCDate = this.from.utc().format('YYYY-MM-DDTHH:mm:ss');
96 | this.toUTCDate = this.to.utc().format('YYYY-MM-DDTHH:mm:ss');
97 | }
98 |
99 | /**
100 | * onChangeParams is invoked every time when route changes
101 | * @param params optional
102 | */
103 | abstract onChangeParams(params): void;
104 |
105 | /**
106 | * set timezone based on given query parameter.
107 | */
108 | setTimeZoneFromParams() {
109 | const tz = this.queryParams.tz || 'browser';
110 | const expireDays = moment().utc().add(7, 'y').toString();
111 | document.cookie = `timezone=${tz}; expires=${expireDays}; path=/`;
112 | }
113 |
114 | setThemeFromParams() {
115 | const theme = this.queryParams.theme || '';
116 | if (theme) {
117 | const expireDays = moment().utc().add(7, 'y').toString();
118 | document.cookie = `theme=app-theme-${theme}; expires=${expireDays}; path=/`;
119 | }
120 | }
121 |
122 | /**
123 | * Destroys route subscription on component unload.
124 | */
125 | ngOnDestroy() {
126 | this.routerSubscription.unsubscribe();
127 | }
128 | }
129 |
130 |
131 | export class QanError extends Error {
132 | static errType = 'QanError';
133 | name = 'QanError';
134 | }
135 |
--------------------------------------------------------------------------------
/src/app/core/core.module.ts:
--------------------------------------------------------------------------------
1 | import {
2 | NgModule,
3 | Optional, SkipSelf
4 | } from '@angular/core';
5 |
6 | import { CommonModule } from '@angular/common';
7 | import { ErrorHandler } from '@angular/core';
8 |
9 | import { SharedModule } from '../shared/shared.module';
10 | import { NavComponent } from './nav/nav.component';
11 | import { JSONTreeComponent } from './json-tree/json-tree.component';
12 | import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
13 | // import { QanErrorHandler } from './qan-error.handler';
14 | import { InstanceService } from './instance.service';
15 | import { CoreComponent } from './core.component';
16 |
17 | import { QueryProfileComponent } from '../query-profile/query-profile.component';
18 | import { QueryProfileService } from '../query-profile/query-profile.service';
19 | import { MySQLQueryDetailsComponent } from '../mysql-query-details/mysql-query-details.component';
20 | import { MySQLQueryDetailsService } from '../mysql-query-details/mysql-query-details.service';
21 | import { MongoQueryDetailsComponent } from '../mongo-query-details/mongo-query-details.component';
22 | import { MongoQueryDetailsService } from '../mongo-query-details/mongo-query-details.service';
23 | import { SummaryComponent } from '../summary/summary.component';
24 | import { SummaryService } from '../summary/summary.service';
25 | import { SettingsComponent } from '../settings/settings.component';
26 | import { SettingsService } from '../settings/settings.service';
27 | import { AddAmazonRDSService } from '../add-amazon-rds/add-amazon-rds.service';
28 | import { AddRemoteInstanceService } from '../add-remote-instances/add-remote-instance.service';
29 | import { RemoteInstancesListService } from '../remote-instances-list/remote-instances-list.service';
30 | import { ClipboardModule } from 'ngx-clipboard';
31 | import {RemoteInstancesListComponent} from '../remote-instances-list/remote-instances-list.component';
32 |
33 | @NgModule({
34 | imports: [CommonModule, SharedModule, ClipboardModule],
35 | declarations: [NavComponent, PageNotFoundComponent, QueryProfileComponent,
36 | MySQLQueryDetailsComponent, MongoQueryDetailsComponent,
37 | SummaryComponent, SettingsComponent, JSONTreeComponent, RemoteInstancesListComponent],
38 | exports: [NavComponent, PageNotFoundComponent, QueryProfileComponent,
39 | MySQLQueryDetailsComponent, MongoQueryDetailsComponent,
40 | SummaryComponent, SettingsComponent, JSONTreeComponent],
41 | providers: [InstanceService, QueryProfileService, MySQLQueryDetailsService,
42 | MongoQueryDetailsService, SummaryService, SettingsService,
43 | AddAmazonRDSService, AddRemoteInstanceService, RemoteInstancesListService]
44 | })
45 | export class CoreModule {
46 |
47 | constructor( @Optional() @SkipSelf() parentModule: CoreModule) {
48 | if (parentModule) {
49 | throw new Error(
50 | 'CoreModule is already loaded. Import it in the AppModule only');
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/app/core/instance.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed, inject } from '@angular/core/testing';
2 |
3 | import { InstanceService } from './instance.service';
4 |
5 | describe('InstanceService', () => {
6 | beforeEach(() => {
7 | TestBed.configureTestingModule({
8 | providers: [InstanceService]
9 | });
10 | });
11 |
12 | it('should ...', inject([InstanceService], (service: InstanceService) => {
13 | expect(service).toBeTruthy();
14 | }));
15 | });
16 |
--------------------------------------------------------------------------------
/src/app/core/instance.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import {HttpClient} from '@angular/common/http';
3 |
4 | export interface Instance {
5 | Created: string;
6 | DSN: string;
7 | Deleted: string;
8 | Distro: string;
9 | Id: number;
10 | Name: string;
11 | ParentUUID: string;
12 | Subsystem: string;
13 | UUID: string;
14 | Version: string;
15 | Agent?: Instance | null;
16 | }
17 |
18 | @Injectable()
19 | export class InstanceService {
20 | private instancesUrl = '/qan-api/instances?deleted=no';
21 | public dbServers: Array = [];
22 | public dbServerMap: { [key: string]: Instance } = {};
23 | constructor(private httpClient: HttpClient) { }
24 |
25 | public getDBServers(): Promise {
26 | return this.httpClient.get(this.instancesUrl)
27 | .toPromise()
28 | .then((response: any) => {
29 | const agents = response.filter(
30 | (i: Instance) => i.Subsystem === 'agent'
31 | ) as Instance[];
32 |
33 | this.dbServers = (response.filter(
34 | (i: Instance) => i.Subsystem === 'mysql' || i.Subsystem === 'mongo'
35 | ) as Instance[]);
36 |
37 | const agentsByParentUUID: { [key: string]: Instance } = {};
38 | for (const agent of agents) {
39 | agentsByParentUUID[agent.ParentUUID] = agent;
40 | }
41 |
42 | for (const srv of this.dbServers) {
43 | this.dbServerMap[srv.Name] = srv;
44 | this.dbServerMap[srv.Name].Agent = agentsByParentUUID[srv.ParentUUID];
45 | }
46 | return this.dbServers;
47 | })
48 | .catch(err => console.log(err));
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/app/core/json-tree/json-tree.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{isCollapsed ? 'Expand All' : 'Collapse All'}}
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/app/core/json-tree/json-tree.component.scss:
--------------------------------------------------------------------------------
1 | // Copied and pasted
2 | ::ng-deep {
3 | .renderjson a { text-decoration: none; }
4 | .renderjson .disclosure { color: crimson;
5 | font-size: 150%; }
6 | .renderjson .syntax { color: grey; }
7 | .renderjson .string { color: red; }
8 | .renderjson .number { color: cyan; }
9 | .renderjson .boolean { color: plum; }
10 | .renderjson .key { color: lightblue; }
11 | .renderjson .keyword { color: lightgoldenrodyellow; }
12 | .renderjson .object.syntax { color: lightseagreen; }
13 | .renderjson .array.syntax { color: lightsalmon; }
14 | }
15 |
16 | .json-tree-wrapper {
17 | display: flex;
18 | justify-content: space-between;
19 | width: 100%;
20 | margin-right: 20px;
21 | }
22 |
23 | .pmm-button {
24 | position: fixed;
25 | right: 175px;
26 | }
27 |
--------------------------------------------------------------------------------
/src/app/core/json-tree/json-tree.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, Input, OnChanges, ElementRef} from '@angular/core';
2 | import * as renderjson from 'renderjson';
3 |
4 | @Component({
5 | selector: 'app-json-tree',
6 | templateUrl: './json-tree.component.html',
7 | styleUrls: ['./json-tree.component.scss']
8 | })
9 |
10 | export class JSONTreeComponent implements OnChanges {
11 | public element: ElementRef;
12 | public isCollapsed = true;
13 |
14 | @Input() public json: any;
15 |
16 | constructor(element: ElementRef) {
17 | this.element = element;
18 | renderjson.set_icons('+', '-');
19 | }
20 |
21 | ngOnChanges() {
22 | this.isCollapsed = true;
23 | this.resetJson();
24 | }
25 |
26 | public toggleAll() {
27 | this.isCollapsed = !this.isCollapsed;
28 | this.resetJson();
29 | }
30 |
31 | public resetJson() {
32 | renderjson.set_show_to_level(this.isCollapsed ? '' : 'all');
33 |
34 | this.element.nativeElement.querySelector('#json-viewer').innerHTML = '';
35 | this.element.nativeElement.querySelector('#json-viewer').appendChild(renderjson(this.json));
36 | }
37 | }
38 |
39 |
40 |
--------------------------------------------------------------------------------
/src/app/core/nav/nav.component.html:
--------------------------------------------------------------------------------
1 |
2 | DEMO
3 |
4 |
5 | Query Analytics
6 |
7 |
8 |
52 |
53 |
54 |
55 | {{ fromDateCompact }} to {{ toDateCompact }} {{ timezone === 'browser' ? '' : 'UTC' }}
56 |
57 |
58 |
59 |
61 |
101 |
102 |
103 |
104 |
105 |
110 |
--------------------------------------------------------------------------------
/src/app/core/nav/nav.component.scss:
--------------------------------------------------------------------------------
1 | .img-db {
2 | width: 16px;
3 | height: 16px;
4 | }
5 |
6 | @media screen {
7 | .scrollable-list {
8 | margin: 0;
9 | padding: 0;
10 | min-width: 600px;
11 | max-width: 900px;
12 | max-height:70vh;
13 | overflow:scroll;
14 | }
15 |
16 | .printable-header {
17 | display: none;
18 | }
19 |
20 | .ribbon {
21 | position: absolute;
22 | right: -5px; top: -5px;
23 | z-index: -2;
24 | overflow: hidden;
25 | width: 75px; height: 75px;
26 | text-align: right;
27 | }
28 | .ribbon span {
29 | font-size: 10px;
30 | font-weight: bold;
31 | color: #FFF;
32 | text-transform: uppercase;
33 | text-align: center;
34 | line-height: 20px;
35 | transform: rotate(45deg);
36 | -webkit-transform: rotate(45deg);
37 | width: 100px;
38 | display: block;
39 | background: #79A70A;
40 | background: linear-gradient(#F79E05 0%, #8F5408 100%);
41 | box-shadow: 0 3px 10px -5px rgba(0, 0, 0, 1);
42 | position: absolute;
43 | top: 19px; right: -21px;
44 | }
45 | .ribbon span::before {
46 | content: "";
47 | position: absolute; left: 0px; top: 100%;
48 | z-index: -1;
49 | border-left: 3px solid #8F5408;
50 | border-right: 3px solid transparent;
51 | border-bottom: 3px solid transparent;
52 | border-top: 3px solid #8F5408;
53 | }
54 | .ribbon span::after {
55 | content: "";
56 | position: absolute; right: 0px; top: 100%;
57 | z-index: -1;
58 | border-left: 3px solid transparent;
59 | border-right: 3px solid #8F5408;
60 | border-bottom: 3px solid transparent;
61 | border-top: 3px solid #8F5408;
62 | }
63 | }
64 |
65 | @media print {
66 | nav {
67 | display: none;
68 | }
69 |
70 | .printable-header {
71 | display: block;
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/app/core/nav/nav.component.spec.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable:no-unused-variable */
2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
3 | import { By } from '@angular/platform-browser';
4 | import { DebugElement } from '@angular/core';
5 |
6 | import { NavComponent } from './nav.component';
7 |
8 | describe('NavComponent', () => {
9 | let component: NavComponent;
10 | let fixture: ComponentFixture;
11 |
12 | beforeEach(async(() => {
13 | TestBed.configureTestingModule({
14 | declarations: [ NavComponent ]
15 | })
16 | .compileComponents();
17 | }));
18 |
19 | beforeEach(() => {
20 | fixture = TestBed.createComponent(NavComponent);
21 | component = fixture.componentInstance;
22 | fixture.detectChanges();
23 | });
24 |
25 | it('should create', () => {
26 | expect(component).toBeTruthy();
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/src/app/core/nav/nav.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnDestroy } from '@angular/core';
2 | import { MomentFormatPipe } from '../../shared/moment-format.pipe';
3 | import { QueryParams, CoreComponent } from '../core.component';
4 | import { Router, ActivatedRoute } from '@angular/router';
5 | import * as moment from 'moment';
6 |
7 | import { environment } from '../../environment';
8 | import {Subscription} from 'rxjs';
9 | import {InstanceService} from '../instance.service';
10 |
11 | @Component({
12 | moduleId: module.id,
13 | selector: 'app-nav',
14 | templateUrl: 'nav.component.html',
15 | styleUrls: ['nav.component.scss']
16 | })
17 | export class NavComponent extends CoreComponent implements OnDestroy {
18 | protected routerSubscription: Subscription;
19 |
20 | public isDemo = false;
21 |
22 | public isExtHidden: boolean;
23 |
24 | public timezone: string;
25 |
26 | public fromDateCompact: string;
27 |
28 | public toDateCompact: string;
29 |
30 | private compactDateFormat = 'MMM D, YYYY HH:mm:ss';
31 |
32 | private fromTimeRaw: string;
33 | private toTimeRaw: string;
34 |
35 | public isValidToInput = true;
36 | public isValidFromInput = true;
37 | public path: string;
38 | public hostSelectorPath = [];
39 |
40 | public constructor(route: ActivatedRoute, router: Router, instanceService: InstanceService) {
41 | super(route, router, instanceService);
42 | const momentFormatPipe = new MomentFormatPipe();
43 | this.isDemo = environment.demoHosts.includes(location.hostname);
44 | this.timezone = momentFormatPipe.getCookie('timezone') || 'browser';
45 | }
46 |
47 | onChangeParams(params) {
48 | // checks changing tz
49 | const momentFormatPipe = new MomentFormatPipe();
50 | this.fromDateCompact = momentFormatPipe.transform(this.from, this.compactDateFormat);
51 | this.toDateCompact = momentFormatPipe.transform(this.to, this.compactDateFormat);
52 | const pathWithParams = this.router.url;
53 | this.path = pathWithParams.substr(0, pathWithParams.indexOf('?'));
54 | this.isExtHidden = true;
55 | this.hostSelectorPath = [];
56 |
57 | if (this.router.url.startsWith('/profile')) {
58 | this.isExtHidden = false;
59 | this.hostSelectorPath = ['/profile'];
60 | }
61 | }
62 |
63 | changeDateInput(event, dir) {
64 | const val = event.trim();
65 | let time = '';
66 | const reg = /^(now)([-/+])(\d+)([yMwdhms])$/;
67 | if (reg.test(val)) {
68 | const [, , sign, num, unit] = reg.exec(val);
69 | switch (sign) {
70 | case '-':
71 | time = moment().subtract(num as moment.unitOfTime.DurationConstructor, unit).valueOf().toString();
72 | break;
73 | case '+':
74 | time = moment().add(num as moment.unitOfTime.DurationConstructor, unit).valueOf().toString();
75 | break;
76 | case '/':
77 | time = moment().startOf(unit as moment.unitOfTime.StartOf).valueOf().toString();
78 | break;
79 | }
80 | } else {
81 | if (val.length > 3) {
82 | try {
83 | if (moment(val, 'YYYY-MM-DD HH:mm').isValid()) {
84 | time = moment(val, 'YYYY-MM-DD HH:mm').valueOf().toString();
85 | }
86 | } catch (err) {
87 | if (dir === 'from') {
88 | this.isValidFromInput = false;
89 | } else {
90 | this.isValidToInput = false;
91 | }
92 | }
93 | }
94 | }
95 |
96 | if (time !== '') {
97 | if (dir === 'from') {
98 | this.isValidFromInput = true;
99 | this.fromTimeRaw = time;
100 | } else {
101 | this.isValidToInput = true;
102 | this.toTimeRaw = time;
103 | }
104 | }
105 | }
106 |
107 | changeDateCal(event, dir) {
108 | if (dir === 'from') {
109 | this.fromTimeRaw = moment([event.year, event.month - 1, event.day]).valueOf().toString();
110 | } else {
111 | this.toTimeRaw = moment([event.year, event.month - 1, event.day]).valueOf().toString();
112 | }
113 | }
114 |
115 | setTimeZone(tz = 'utc') {
116 | this.timezone = tz;
117 | const expireDays = moment().utc().add(7, 'y').toString();
118 | document.cookie = `timezone=${tz}; expires=${expireDays}; path=/`;
119 | const params: QueryParams = Object.assign({}, this.queryParams);
120 | params.tz = tz;
121 | this.router.navigate([this.path], { queryParams: params, relativeTo: this.route });
122 | }
123 |
124 | setQuickRange(num: string, unit = 's') {
125 | const params: QueryParams = Object.assign({}, this.queryParams);
126 | params.to = moment().valueOf().toString();
127 | params.from = moment().subtract(num as moment.unitOfTime.DurationConstructor, unit).valueOf().toString();
128 | this.router.navigate(['profile'], { queryParams: params });
129 | }
130 |
131 | setTimeRange(from, to) {
132 | const params: QueryParams = Object.assign({}, this.queryParams);
133 | params.to = this.toTimeRaw;
134 | params.from = this.fromTimeRaw;
135 | this.router.navigate(['profile'], { queryParams: params });
136 | }
137 |
138 | getDBLogo(distro: string): string {
139 | let src: string;
140 | switch (true) {
141 | case distro.indexOf('Percona Server') !== -1:
142 | src = 'assets/percona-server-black-50.png';
143 | break;
144 | case distro.indexOf('Percona XtraDB') !== -1:
145 | src = 'assets/Percona_XtraDB_Cluster.png';
146 | break;
147 | default:
148 | src = 'assets/database.png';
149 | break;
150 | }
151 | return src;
152 | }
153 |
154 | ngOnDestroy() {
155 | super.ngOnDestroy();
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/src/app/core/page-not-found/page-not-found.component.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Page Not Found
5 |
6 |
--------------------------------------------------------------------------------
/src/app/core/page-not-found/page-not-found.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/percona/qan-app/dc15e33d41a0b87abec08abb83aded5b0f83356a/src/app/core/page-not-found/page-not-found.component.scss
--------------------------------------------------------------------------------
/src/app/core/page-not-found/page-not-found.component.spec.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable:no-unused-variable */
2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
3 | import { By } from '@angular/platform-browser';
4 | import { DebugElement } from '@angular/core';
5 |
6 | import { PageNotFoundComponent } from './page-not-found.component';
7 |
8 | describe('PageNotFoundComponent', () => {
9 | let component: PageNotFoundComponent;
10 | let fixture: ComponentFixture;
11 |
12 | beforeEach(async(() => {
13 | TestBed.configureTestingModule({
14 | declarations: [ PageNotFoundComponent ]
15 | })
16 | .compileComponents();
17 | }));
18 |
19 | beforeEach(() => {
20 | fixture = TestBed.createComponent(PageNotFoundComponent);
21 | component = fixture.componentInstance;
22 | fixture.detectChanges();
23 | });
24 |
25 | it('should create', () => {
26 | expect(component).toBeTruthy();
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/src/app/core/page-not-found/page-not-found.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 |
3 | @Component({
4 | selector: 'app-page-not-found',
5 | templateUrl: './page-not-found.component.html',
6 | styleUrls: ['./page-not-found.component.scss']
7 | })
8 | export class PageNotFoundComponent implements OnInit {
9 |
10 | constructor() { }
11 |
12 | ngOnInit() {
13 | }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/src/app/core/qan-error.handler.ts:
--------------------------------------------------------------------------------
1 | import { ErrorHandler } from '@angular/core';
2 |
3 | export class QanError {
4 |
5 | public name = 'QanError';
6 | public message = '';
7 |
8 | constructor(message: string) {
9 | this.message = message;
10 | }
11 | }
12 |
13 | export class QanErrorHandler extends ErrorHandler {
14 |
15 | handleError(error: any): void {
16 | // console.dir(error);
17 | // console.log(`QanErrorHandler: `, error.rejection.originalError.message);
18 |
19 | try {
20 | super.handleError(error);
21 | } catch (err) {
22 | console.log('hhhh', err);
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/app/environment.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: false,
3 | demoHosts: ['163.172.51.248', 'pmmdemo.percona.com'],
4 | version: '1.0.0'
5 | };
6 |
--------------------------------------------------------------------------------
/src/app/mongo-query-details/mongo-query-details.component.scss:
--------------------------------------------------------------------------------
1 | .data-output {
2 | position: relative;
3 | overflow-y: scroll;
4 | }
5 |
6 | .pmm-button {
7 | position: fixed;
8 | right: 15px;
9 | }
10 |
11 | section.explain {
12 | display: flex;
13 | flex-direction: column;
14 | }
15 |
16 | .metrics-table {
17 | .grid-container {
18 | grid-template-columns: 2fr 2fr 3fr 2fr;
19 |
20 | .cell-overlay {
21 | height: 100%;
22 | }
23 | }
24 |
25 | .grid-container:not(:last-child) {
26 | height: 100%;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/app/mongo-query-details/mongo-query-details.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, OnInit } from '@angular/core';
2 | import { Router, ActivatedRoute } from '@angular/router';
3 | import { Instance, InstanceService } from '../core/instance.service';
4 | import { CoreComponent, QueryParams } from '../core/core.component';
5 | import { MongoQueryDetailsService, QueryDetails } from './mongo-query-details.service';
6 | import * as hljs from 'highlight.js';
7 | import * as vkbeautify from 'vkbeautify';
8 | import * as moment from 'moment';
9 |
10 | @Component({
11 | moduleId: module.id,
12 | selector: 'app-query-details',
13 | templateUrl: './mongo-query-details.component.html',
14 | styleUrls: ['./mongo-query-details.component.scss']
15 | })
16 | export class MongoQueryDetailsComponent extends CoreComponent implements OnInit {
17 |
18 | protected queryID: string;
19 | public queryDetails: any | QueryDetails;
20 | public fingerprint: string;
21 | public queryExample: string;
22 | public classicExplain;
23 | public jsonExplainString;
24 | public jsonExplain;
25 | public errExplain;
26 | protected dbName: string;
27 | public dbTblNames: string;
28 | isCopied = {
29 | queryExample: false,
30 | fingerprint: false,
31 | jsonExplain: false
32 | };
33 | accordionIds = {
34 | serverSummary: ['metrics-table'],
35 | querySection: ['query-fingerprint'],
36 | explainSection: ['json-explain'],
37 | };
38 | isSummary: boolean;
39 | isLoading: boolean;
40 | isExplainLoading: boolean;
41 | isFirstSeen: boolean;
42 | firstSeen: string;
43 | lastSeen: string;
44 | event = new Event('showSuccessNotification');
45 |
46 | constructor(protected route: ActivatedRoute, protected router: Router,
47 | protected instanceService: InstanceService, protected queryDetailsService: MongoQueryDetailsService) {
48 | super(route, router, instanceService);
49 | }
50 |
51 | ngOnInit() {
52 | this.queryParams = this.route.snapshot.queryParams as QueryParams;
53 | this.parseParams();
54 | this.onChangeParams(this.queryParams);
55 | }
56 |
57 | showSuccessNotification(key) {
58 | this.isCopied[key] = true;
59 | setTimeout( () => { this.isCopied[key] = false }, 3000);
60 | window.parent.document.dispatchEvent(this.event);
61 | }
62 |
63 | onChangeParams(params) {
64 | if (!this.dbServer) { return; }
65 | if (['TOTAL', undefined].indexOf(this.queryParams.queryID) !== -1) {
66 | this.isSummary = true;
67 | this.getServerSummary(this.dbServer.UUID, this.fromUTCDate, this.toUTCDate);
68 | } else {
69 | this.isSummary = false;
70 | this.getQueryDetails(this.dbServer.UUID, this.queryParams.queryID, this.fromUTCDate, this.toUTCDate);
71 | this.accordionIds = {
72 | serverSummary: ['metrics-table'],
73 | querySection: ['query-fingerprint'],
74 | explainSection: ['json-explain'],
75 | };
76 | }
77 | }
78 |
79 | getQueryDetails(dbServerUUID, queryID, from, to: string) {
80 | this.isLoading = true;
81 | this.dbName = this.dbTblNames = '';
82 | this.queryExample = '';
83 | this.queryDetailsService.getQueryDetails(dbServerUUID, queryID, from, to)
84 | .then(data => {
85 | this.queryDetails = data;
86 | this.firstSeen = moment(this.queryDetails.Query.FirstSeen).calendar(null, {sameElse: 'lll'});
87 | this.lastSeen = moment(this.queryDetails.Query.LastSeen).calendar(null, {sameElse: 'lll'});
88 | this.fingerprint = this.queryDetails.Query.Fingerprint;
89 | this.queryExample = hljs.highlight('json', vkbeautify.json(this.queryDetails.Example.Query)).value;
90 | this.isFirstSeen = moment.utc(this.queryDetails.Query.FirstSeen).valueOf() > moment.utc(this.fromUTCDate).valueOf();
91 | this.isLoading = false;
92 | })
93 | .then(() => this.getExplain())
94 | .catch(err => console.log(err));
95 | }
96 |
97 | getServerSummary(dbServerUUID: string, from: string, to: string) {
98 | this.dbName = this.dbTblNames = '';
99 | this.queryDetailsService.getSummary(dbServerUUID, from, to)
100 | .then(data => {
101 | this.queryDetails = data;
102 | })
103 | .catch(err => console.log(err));
104 | }
105 |
106 | async getExplain() {
107 | if (!this.dbServer || !this.dbServer.Agent) { return; }
108 | this.isExplainLoading = true;
109 | this.jsonExplain = '';
110 | this.errExplain = '';
111 | const agentUUID = this.dbServer.Agent.UUID;
112 | const dbServerUUID = this.dbServer.UUID;
113 | if (this.dbName === '') {
114 | this.dbName = this.getDBName();
115 | }
116 |
117 | const query = this.queryDetails.Example.Query;
118 | const data = await this.queryDetailsService.getExplain(agentUUID, dbServerUUID, this.dbName, query);
119 | try {
120 | if (data['Error'] === '') {
121 | const jsonSection = JSON.parse(atob(data['Data'])).JSON;
122 | this.jsonExplain = typeof jsonSection === 'string' ? JSON.parse(jsonSection) : jsonSection;
123 | this.jsonExplainString = JSON.stringify(this.jsonExplain);
124 | } else {
125 | this.errExplain = data['Error']
126 | }
127 | this.isExplainLoading = false;
128 | } catch (err) {
129 | console.log(err);
130 | this.isExplainLoading = false;
131 | }
132 | }
133 |
134 | getTableName(): string {
135 | if (this.queryDetails.hasOwnProperty('Query')
136 | && this.queryDetails.Query.hasOwnProperty('Tables')
137 | && this.queryDetails.Query.Tables !== null
138 | && this.queryDetails.Query.Tables.length > 0) {
139 | return this.queryDetails.Query.Tables[0].Table;
140 | }
141 | return '';
142 | }
143 |
144 | private getDBName(): string {
145 | if (this.queryDetails.Example.Db !== '') {
146 | return this.queryDetails.Example.Db;
147 | } else if (this.queryDetails.hasOwnProperty('Query')
148 | && this.queryDetails.Query.hasOwnProperty('Tables')
149 | && this.queryDetails.Query.Tables !== null
150 | && this.queryDetails.Query.Tables.length > 0) {
151 | return this.queryDetails.Query.Tables[0].Db;
152 | }
153 | return '';
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/src/app/mongo-query-details/mongo-query-details.service.spec.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable:no-unused-variable */
2 |
3 | import { TestBed, async, inject } from '@angular/core/testing';
4 | import { MongoQueryDetailsService } from './mongo-query-details.service';
5 |
6 | describe('QueryDetailsService', () => {
7 | beforeEach(() => {
8 | TestBed.configureTestingModule({
9 | providers: [MongoQueryDetailsService]
10 | });
11 | });
12 |
13 | it('should ...', inject([MongoQueryDetailsService], (service: MongoQueryDetailsService) => {
14 | expect(service).toBeTruthy();
15 | }));
16 | });
17 |
--------------------------------------------------------------------------------
/src/app/mongo-query-details/mongo-query-details.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core';
2 | import {HttpClient, HttpHeaders, HttpParams} from '@angular/common/http';
3 |
4 | export interface QueryClass {
5 | Id: string;
6 | Abstract: string;
7 | Fingerprint: string;
8 | Tables: Array<{ Db: string, Table: string }> | null;
9 | FirstSeen: string;
10 | LastSeen: string;
11 | Status: string;
12 | };
13 |
14 | export interface QueryExample {
15 | QueryId: string;
16 | InstanceUUID: string;
17 | Period: string;
18 | Ts: string;
19 | Db: string;
20 | QueryTime: number;
21 | Query: string;
22 | };
23 |
24 | export interface QueryDetails {
25 | InstanceId: string;
26 | Begin: string;
27 | End: string;
28 | Query: QueryClass;
29 | Example: QueryExample;
30 | Metrics2: {};
31 | Sparks2: Array<{}>;
32 | };
33 |
34 | export interface ServerSummary {
35 | InstanceId: string;
36 | Begin: string;
37 | End: string;
38 | Metrics2: {};
39 | Sparks2: Array<{}>;
40 | };
41 |
42 | @Injectable()
43 | export class MongoQueryDetailsService {
44 |
45 | private headers = new HttpHeaders({ 'Content-Type': 'application/json' });
46 |
47 | constructor(private httpClient: HttpClient) { }
48 |
49 | getQueryDetails(dbServerUUID, queryUUID, begin, end: string): Promise {
50 | const url = `/qan-api/qan/report/${dbServerUUID}/query/${queryUUID}`;
51 |
52 | const params = new HttpParams()
53 | .set('begin', begin)
54 | .set('end', end);
55 |
56 | return this.httpClient
57 | .get(url, { headers: this.headers, params: params })
58 | .toPromise()
59 | .then(response => response as QueryDetails)
60 | .catch(err => console.log(err));
61 | }
62 |
63 | getSummary(dbServerUUID: string, begin: string, end: string): Promise {
64 | const url = `/qan-api/qan/report/${dbServerUUID}/server-summary`;
65 |
66 | const params = new HttpParams()
67 | .set('begin', begin)
68 | .set('end', end);
69 |
70 | return this.httpClient
71 | .get(url, { headers: this.headers, params: params })
72 | .toPromise()
73 | .then(response => response as ServerSummary)
74 | .catch(err => console.log(err));
75 | }
76 |
77 | getTableInfo(agentUUID: string, dbServerUUID: string, dbName: string, tblName: string) {
78 | const url = `/qan-api/agents/${agentUUID}/cmd`;
79 |
80 | const data = {
81 | UUID: dbServerUUID,
82 | Create: [{
83 | Db: dbName,
84 | Table: tblName
85 | }],
86 | Index: [{
87 | Db: dbName,
88 | Table: tblName
89 | }],
90 | Status: [{
91 | Db: dbName,
92 | Table: tblName
93 | }]
94 | };
95 |
96 | const params = {
97 | AgentUUID: agentUUID,
98 | Service: 'query',
99 | Cmd: 'TableInfo',
100 | Data: btoa(JSON.stringify(data))
101 | };
102 |
103 | return this.httpClient
104 | .put(url, params)
105 | .toPromise()
106 | .then(response => JSON.parse(atob(response['Data'])))
107 | .catch(err => console.error(err));
108 | }
109 |
110 | async getExplain(agentUUID: string, dbServerUUID: string, dbName: string, query: string) {
111 | const url = `/qan-api/agents/${agentUUID}/cmd`;
112 | const data = {
113 | UUID: dbServerUUID,
114 | Db: dbName,
115 | Query: query,
116 | Convert: true // agent will convert if not SELECT and MySQL <= 5.5 or >= 5.6 but no privs
117 | };
118 |
119 | const params = {
120 | AgentUUID: agentUUID,
121 | Service: 'query',
122 | Cmd: 'Explain',
123 | Data: btoa(JSON.stringify(data))
124 | };
125 |
126 | return await this.httpClient
127 | .put(url, params)
128 | .toPromise()
129 | }
130 |
131 | updateTables(queryID: string, dbTables: Array<{}>) {
132 | const url = `/qan-api/queries/${queryID}/tables`;
133 | return this.httpClient
134 | .put(url, dbTables)
135 | .toPromise()
136 | .then(resp => console.log(resp))
137 | .catch(err => console.error(err));
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/src/app/mysql-query-details/mysql-query-details.component.scss:
--------------------------------------------------------------------------------
1 | .data-output {
2 | position: relative;
3 | overflow-y: scroll;
4 | }
5 |
6 | .pmm-button {
7 | position: fixed;
8 | right: 15px;
9 | }
10 |
11 | section.explain {
12 | display: flex;
13 | flex-direction: column;
14 | }
15 |
16 | .query-panel {
17 | margin-bottom: 20px;
18 | }
19 |
20 | .grid-container {
21 | grid-template-columns: 2fr 2fr 3fr 2fr;
22 | }
23 |
24 | .classic-table-wrapper {
25 | display: table;
26 | border-collapse: collapse;
27 | width: 100%;
28 |
29 | .classic-table-row {
30 | display: table-row;
31 | }
32 |
33 | .classic-table-row > div {
34 | display: table-cell;
35 | height: 100%;
36 | padding: 5px !important;
37 | vertical-align: middle;
38 | }
39 | }
40 |
41 | .status-table-wrapper {
42 | .grid-container {
43 | grid-template-columns: repeat(2, 1fr);
44 | }
45 | }
46 |
47 | .indexes-table {
48 | .grid-container-wrapper {
49 | display: flex;
50 | flex-direction: column;
51 |
52 | & > div {
53 | border-right: none !important;
54 | }
55 | }
56 |
57 | .table-body.grid-container,
58 | .table-header.grid-container {
59 | width: 100%;
60 | grid-template-columns: 1fr 1fr 0.5fr 0.5fr 1fr 1fr 1fr 0.5fr 1fr;
61 | }
62 |
63 | .table-body.grid-container > div:not(:last-child) {
64 | border-bottom: none !important;
65 | height: 100%;
66 | display: flex;
67 | align-items: center;
68 | }
69 | }
70 |
71 | .table-wrapper.accordion-table .table-body-row.grid-container {
72 | border-right: none;
73 | }
74 |
75 | .tables-input {
76 | max-width: 420px;
77 |
78 | .form-control {
79 | max-width: 320px;
80 | padding-right: 45px;
81 | }
82 | }
83 |
84 | .table-buttons-wrapper {
85 | margin-bottom: 20px;
86 |
87 | .btn-group {
88 | margin-right: 30px;
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/src/app/mysql-query-details/mysql-query-details.service.spec.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable:no-unused-variable */
2 |
3 | import { TestBed, async, inject } from '@angular/core/testing';
4 | import { MySQLQueryDetailsService } from './mysql-query-details.service';
5 |
6 | describe('QueryDetailsService', () => {
7 | beforeEach(() => {
8 | TestBed.configureTestingModule({
9 | providers: [MySQLQueryDetailsService]
10 | });
11 | });
12 |
13 | it('should ...', inject([MySQLQueryDetailsService], (service: MySQLQueryDetailsService) => {
14 | expect(service).toBeTruthy();
15 | }));
16 | });
17 |
--------------------------------------------------------------------------------
/src/app/mysql-query-details/mysql-query-details.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core';
2 | import {HttpClient, HttpHeaders, HttpParams} from '@angular/common/http';
3 |
4 | export interface QueryClass {
5 | Id: string;
6 | Abstract: string;
7 | Fingerprint: string;
8 | Tables: Array<{ Db: string, Table: string }> | null;
9 | FirstSeen: string;
10 | LastSeen: string;
11 | Status: string;
12 | };
13 |
14 | export interface QueryExample {
15 | QueryId: string;
16 | InstanceUUID: string;
17 | Period: string;
18 | Ts: string;
19 | Db: string;
20 | QueryTime: number;
21 | Query: string;
22 | };
23 |
24 | export interface QueryDetails {
25 | InstanceId: string;
26 | Begin: string;
27 | End: string;
28 | Query: QueryClass;
29 | Example: QueryExample;
30 | Metrics2: {};
31 | Sparks2: Array<{}>;
32 | };
33 |
34 | export interface ServerSummary {
35 | InstanceId: string;
36 | Begin: string;
37 | End: string;
38 | Metrics2: {};
39 | Sparks2: Array<{}>;
40 | };
41 |
42 | @Injectable()
43 | export class MySQLQueryDetailsService {
44 |
45 | private headers = new HttpHeaders({'Content-Type': 'application/json'});
46 |
47 | constructor(private httpClient: HttpClient) {
48 | }
49 |
50 | public async getQueryDetails(dbServerUUID, queryUUID, begin, end: string): Promise {
51 | const url = `/qan-api/qan/report/${dbServerUUID}/query/${queryUUID}`;
52 | const params = new HttpParams()
53 | .set('begin', begin)
54 | .set('end', end);
55 |
56 | const response = await this.httpClient
57 | .get(url, {headers: this.headers, params: params})
58 | .toPromise();
59 | return response as QueryDetails;
60 | }
61 |
62 | public async getSummary(dbServerUUID: string, begin: string, end: string): Promise {
63 | const url = `/qan-api/qan/report/${dbServerUUID}/server-summary`;
64 |
65 | const params = new HttpParams()
66 | .set('begin', begin)
67 | .set('end', end);
68 |
69 | const response = await this.httpClient
70 | .get(url, {headers: this.headers, params: params})
71 | .toPromise();
72 | return response as ServerSummary;
73 | }
74 |
75 | getTableInfo(agentUUID: string, dbServerUUID: string, dbName: string, tblName: string) {
76 | const url = `/qan-api/agents/${agentUUID}/cmd`;
77 |
78 | const data = {
79 | UUID: dbServerUUID,
80 | Create: [{
81 | Db: dbName,
82 | Table: tblName
83 | }],
84 | Index: [{
85 | Db: dbName,
86 | Table: tblName
87 | }],
88 | Status: [{
89 | Db: dbName,
90 | Table: tblName
91 | }]
92 | };
93 |
94 | const params = {
95 | AgentUUID: agentUUID,
96 | Service: 'query',
97 | Cmd: 'TableInfo',
98 | Data: btoa(JSON.stringify(data))
99 | };
100 |
101 | return this.httpClient
102 | .put(url, params)
103 | .toPromise()
104 | .then(response => JSON.parse(atob(response['Data'])));
105 | }
106 |
107 | getExplain(agentUUID: string, dbServerUUID: string, dbName: string, query: string) {
108 | const url = `/qan-api/agents/${agentUUID}/cmd`;
109 | const data = {
110 | UUID: dbServerUUID,
111 | Db: dbName,
112 | Query: query,
113 | Convert: true // agent will convert if not SELECT and MySQL <= 5.5 or >= 5.6 but no privs
114 | };
115 |
116 | const params = {
117 | AgentUUID: agentUUID,
118 | Service: 'query',
119 | Cmd: 'Explain',
120 | Data: btoa(JSON.stringify(data))
121 | };
122 |
123 | return this.httpClient
124 | .put(url, params)
125 | .toPromise()
126 | .then(response => response);
127 | }
128 |
129 | updateTables(queryID: string, dbTables: Array<{}>) {
130 | const url = `/qan-api/queries/${queryID}/tables`;
131 | return this.httpClient
132 | .put(url, dbTables)
133 | .toPromise()
134 | .then(resp => console.log(resp));
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/src/app/query-profile/query-profile.component.scss:
--------------------------------------------------------------------------------
1 | .table-header {
2 | display: flex;
3 | flex-direction: row;
4 | justify-content: space-between;
5 | align-items: center;
6 | margin-bottom: 15px;
7 |
8 | .table-title {
9 | max-width: 440px;
10 | width: 100%;
11 | font-size: 18px;
12 | }
13 |
14 | .display-options {
15 | display: flex;
16 | justify-content: space-between;
17 | align-items: center;
18 |
19 | .option-container {
20 | height: 40px;
21 | display: flex;
22 | align-items: center;
23 | overflow: hidden;
24 | margin-left: 10px;
25 | box-sizing: border-box;
26 | }
27 | }
28 | }
29 |
30 | .table-body {
31 | margin-bottom: 15px;
32 | display: flex;
33 | flex-direction: column;
34 | box-sizing: border-box;
35 |
36 | .grid-container {
37 | display: grid;
38 | grid-template-columns: 1fr 9fr 7fr 10fr 9fr;
39 | grid-template-rows: 40px;
40 | box-sizing: border-box;
41 | outline: none;
42 |
43 | &:active,
44 | &:visited,
45 | &:hover {
46 | outline: none;
47 | }
48 |
49 | &:first-child {
50 | font-weight: 500;
51 | font-size: 12px;
52 | }
53 |
54 | &.table-selected > div:first-child,
55 | &.first-seen-query > div:first-child{
56 | padding-left: 8px !important;
57 | border-left: 3px solid #2e5785 !important;
58 | }
59 |
60 | &.table-selected {
61 | outline: none;
62 |
63 | &:active,
64 | &:visited,
65 | &:hover {
66 | outline: none
67 | }
68 | }
69 |
70 | > div {
71 | border-left: none;
72 | border-bottom: none;
73 | box-sizing: border-box;
74 | display: flex;
75 | align-items: center;
76 | justify-content: space-between;
77 | padding: 0 10px;
78 |
79 | &:first-child {
80 | padding-right: 0;
81 | border-left: 1px solid;
82 | }
83 | }
84 | }
85 | }
86 |
87 | .load-more-wrapper {
88 | display: flex;
89 | justify-content: center;
90 | align-items: center;
91 | margin-bottom: 20px;
92 | }
93 |
94 | .main-table-overlay {
95 | display: block;
96 | width: 30%;
97 | height: 30px;
98 | }
99 |
100 | .total-queries-amount {
101 | max-width: 350px;
102 | height: 40px;
103 | width: auto;
104 | padding: 0 15px;
105 | cursor: pointer;
106 | }
107 |
108 | input:disabled {
109 | cursor: not-allowed !important;
110 | opacity: .65;
111 | z-index: 100;
112 | }
113 |
--------------------------------------------------------------------------------
/src/app/query-profile/query-profile.component.ts:
--------------------------------------------------------------------------------
1 | import { CoreComponent, QueryParams, QanError } from '../core/core.component';
2 | import { Component } from '@angular/core';
3 | import { InstanceService } from '../core/instance.service';
4 | import { QueryProfileService } from './query-profile.service';
5 | import { Router, ActivatedRoute } from '@angular/router';
6 | import * as moment from 'moment';
7 | import { MomentFormatPipe } from '../shared/moment-format.pipe';
8 |
9 | const queryProfileError = 'No data. Please check pmm-client and database configurations on selected instance.';
10 |
11 | @Component({
12 | moduleId: module.id,
13 | templateUrl: 'query-profile.component.html',
14 | styleUrls: ['./query-profile.component.scss'],
15 | })
16 | export class QueryProfileComponent extends CoreComponent {
17 |
18 | public queryProfile: Array<{}>;
19 | public profileTotal;
20 | public offset: number;
21 | public totalAmountOfQueries: number;
22 | public leftInDbQueries: number;
23 | public searchValue: string;
24 | public fromDate: string;
25 | public toDate: string;
26 | public isLoading: boolean;
27 | public isQuerySwitching: boolean;
28 | public noQueryError: string;
29 | public isFirstSeen: boolean;
30 | public isFirsSeenChecked = false;
31 | public isSearchQuery = false;
32 |
33 | constructor(
34 | protected route: ActivatedRoute,
35 | protected router: Router,
36 | protected instanceService: InstanceService,
37 | protected queryProfileService: QueryProfileService
38 | ) {
39 | super(route, router, instanceService);
40 | }
41 |
42 | onChangeParams(params) {
43 | // checks changing tz
44 | const momentFormatPipe = new MomentFormatPipe();
45 | this.fromDate = momentFormatPipe.transform(this.from, 'llll');
46 | this.toDate = momentFormatPipe.transform(this.to, 'llll');
47 | // only if host, from and to are diffrent from prev router - load queries.
48 | if (!this.previousQueryParams ||
49 | this.previousQueryParams['var-host'] !== this.queryParams['var-host'] ||
50 | this.previousQueryParams.from !== this.queryParams.from ||
51 | this.previousQueryParams.to !== this.queryParams.to ||
52 | this.previousQueryParams.search !== this.queryParams.search ||
53 | this.previousQueryParams.first_seen !== this.queryParams.first_seen ||
54 | this.previousQueryParams.tz !== this.queryParams.tz) {
55 | this.loadQueries();
56 | }
57 | }
58 |
59 | checkFirstSeen(currentQuery) {
60 | this.isFirstSeen = moment.utc(currentQuery['FirstSeen']).valueOf() > moment.utc(this.fromUTCDate).valueOf();
61 | return this.isFirstSeen;
62 | }
63 |
64 | public async loadQueries() {
65 | this.dbServer = this.instanceService.dbServers[0];
66 | for (const dbServer of this.instanceService.dbServers) {
67 | if (dbServer.Name === this.queryParams['var-host']) {
68 | this.dbServer = dbServer;
69 | }
70 | }
71 | this.isQuerySwitching = true;
72 |
73 | // clear after error
74 | this.noQueryError = '';
75 | this.totalAmountOfQueries = this.leftInDbQueries = 0;
76 | this.queryProfile = [];
77 | this.searchValue = this.queryParams.search === 'null' ? '' : this.queryParams.search;
78 | const search = this.queryParams.search === 'null' && this.searchValue !== 'NULL' ? '' : this.queryParams.search;
79 | const firstSeen = this.queryParams.first_seen;
80 | this.offset = 0;
81 | try {
82 | const data = await this.queryProfileService
83 | .getQueryProfile(this.dbServer.UUID, this.fromUTCDate, this.toUTCDate, this.offset, search, firstSeen);
84 | if (data.hasOwnProperty('Error') && data['Error'] !== '') {
85 | throw new QanError('Queries are not available.');
86 | }
87 | this.totalAmountOfQueries = data['TotalQueries'];
88 | if (this.totalAmountOfQueries > 0) {
89 | this.queryProfile = data['Query'];
90 | this.leftInDbQueries = this.totalAmountOfQueries - (this.queryProfile.length - 1);
91 | this.profileTotal = this.queryProfile[0];
92 | }
93 | } catch (err) {
94 | console.error(err);
95 | this.noQueryError = err.name === QanError.errType ? err.message : queryProfileError;
96 | } finally {
97 | this.isQuerySwitching = false;
98 | }
99 | }
100 |
101 | public async loadMoreQueries() {
102 | this.isLoading = true;
103 | const dbServerUUID = this.dbServer.UUID;
104 | this.offset = this.offset + 10;
105 | const search =
106 | this.queryParams.search === 'null' &&
107 | this.searchValue !== 'NULL' && this.searchValue !== 'null' ? '' : this.queryParams.search;
108 | const firstSeen = this.queryParams.first_seen;
109 | const data = await this.queryProfileService
110 | .getQueryProfile(dbServerUUID, this.fromUTCDate, this.toUTCDate, this.offset, search, firstSeen);
111 |
112 | const _ = data['Query'].shift();
113 | for (const q of data['Query']) {
114 | this.queryProfile.push(q);
115 | }
116 | this.leftInDbQueries = this.totalAmountOfQueries - (this.queryProfile.length - 1);
117 | this.isLoading = false;
118 | }
119 |
120 | composeQueryParamsForGrid(queryID: string | null): QueryParams {
121 | const queryParams: QueryParams = Object.assign({}, this.queryParams);
122 | queryParams.queryID = queryID || 'TOTAL';
123 | return queryParams;
124 | }
125 |
126 | search() {
127 | this.isSearchQuery = true;
128 | const params: QueryParams = Object.assign({}, this.queryParams);
129 | if (!!this.searchValue) {
130 | params.search = this.searchValue === 'null' ? 'NULL' : this.searchValue;
131 | } else {
132 | delete params.search;
133 | }
134 | delete params.queryID;
135 | this.router.navigate(['profile'], { queryParams: params });
136 | }
137 |
138 | getFirstSeen(isFirsSeenChecked = false) {
139 | this.isQuerySwitching = true;
140 | this.isFirsSeenChecked = isFirsSeenChecked;
141 | const params: QueryParams = Object.assign({}, this.queryParams);
142 | if (isFirsSeenChecked) {
143 | params.first_seen = this.isFirsSeenChecked;
144 | } else {
145 | delete params.first_seen;
146 | }
147 | delete params.queryID;
148 | this.router.navigate(['profile'], { queryParams: params });
149 | this.isQuerySwitching = false;
150 | }
151 | }
152 |
--------------------------------------------------------------------------------
/src/app/query-profile/query-profile.service.spec.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable:no-unused-variable */
2 |
3 | import { TestBed, async, inject } from '@angular/core/testing';
4 | import { QueryProfileService } from './query-profile.service';
5 |
6 | describe('QueryProfileService', () => {
7 | beforeEach(() => {
8 | TestBed.configureTestingModule({
9 | providers: [QueryProfileService]
10 | });
11 | });
12 |
13 | it('should ...', inject([QueryProfileService], (service: QueryProfileService) => {
14 | expect(service).toBeTruthy();
15 | }));
16 | });
17 |
--------------------------------------------------------------------------------
/src/app/query-profile/query-profile.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core';
2 | import {HttpClient, HttpHeaders, HttpParams} from '@angular/common/http';
3 |
4 | @Injectable()
5 | export class QueryProfileService {
6 |
7 | private headers = new HttpHeaders({'Content-Type': 'application/json'});
8 |
9 | constructor(private httpClient: HttpClient) {
10 | }
11 |
12 | public async getQueryProfile(dbServerUUID, begin, end: string,
13 | offset = 0, search = '', first_seen): Promise<{}> {
14 | const url = `/qan-api/qan/profile/${dbServerUUID}`;
15 | const searchValue = btoa(
16 | search.replace(/%([0-9A-F]{2})/g,
17 | (match, p1) => String.fromCharCode(Number('0x' + p1)))
18 | );
19 | const params = new HttpParams()
20 | .set('begin', begin)
21 | .set('end', end)
22 | .set('offset', String(offset))
23 | .set('first_seen', String(!!first_seen))
24 | .set('search', searchValue);
25 |
26 | return await this.httpClient
27 | .get(url, {headers: this.headers, params: params})
28 | .toPromise();
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/app/remote-instances-list/remote-instances-list.component.html:
--------------------------------------------------------------------------------
1 | Amazon RDS and remote instances
2 |
3 |
18 |
19 | {{ instance.node.name }}
20 | {{ instance.service.address }}:{{ instance.service.port }}
21 | {{ instance.node.region }}
22 | {{ instance.service.engine }} {{ instance.service.engine_version }}
23 |
24 |
25 |
26 |
27 |
28 |
33 |
34 |
35 | {{ errorMessage }}
36 |
37 |
--------------------------------------------------------------------------------
/src/app/remote-instances-list/remote-instances-list.component.scss:
--------------------------------------------------------------------------------
1 | h3 {
2 | margin-bottom: 1.5rem;
3 | }
4 |
5 | .grid-table {
6 | display: grid;
7 | grid-template-rows: minmax(25px, auto);
8 | grid-auto-rows: minmax(40px, auto);
9 | border: 1px solid #292929;
10 | }
11 |
12 | .grid-table > *:not(:last-child) {
13 | border-bottom: 1px solid #292929;
14 | }
15 |
16 | .grid-table__row > *:last-child {
17 | display: flex;
18 | justify-content: center;
19 | align-items: center;
20 | }
21 |
22 | .grid-table__row {
23 | display: grid;
24 | grid-template-columns: 2.5fr 6fr 1.5fr 2fr 80px;
25 | }
26 |
27 | .grid-table__row > * {
28 | display: flex;
29 | align-items: center;
30 | padding: 0 10px;
31 | word-break: break-all;
32 | }
33 |
34 | .grid-table__row > *:not(.row__cell--highlight) {
35 | border-right: 1px solid #292929;
36 | }
37 |
38 | .grid-table__row > *:last-child {
39 | border-right: none;
40 | }
41 |
42 | .row__cell--highlight {
43 | border: 1px solid white;
44 | }
45 |
46 | .fa.fa-trash-o {
47 | cursor: pointer;
48 | color: #444444;
49 | font-size: 1.2rem;
50 | }
51 |
52 | .pointer {
53 | cursor: pointer;
54 | }
55 |
56 | .sort-icon {
57 | margin-left: 10px;
58 | }
59 |
--------------------------------------------------------------------------------
/src/app/remote-instances-list/remote-instances-list.component.spec.ts:
--------------------------------------------------------------------------------
1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
2 |
3 | import { RemoteInstancesListComponent } from './remote-instances-list.component';
4 |
5 | describe('RemoteInstancesListComponent', () => {
6 | let component: RemoteInstancesListComponent;
7 | let fixture: ComponentFixture;
8 |
9 | beforeEach(async(() => {
10 | TestBed.configureTestingModule({
11 | declarations: [ RemoteInstancesListComponent ]
12 | })
13 | .compileComponents();
14 | }));
15 |
16 | beforeEach(() => {
17 | fixture = TestBed.createComponent(RemoteInstancesListComponent);
18 | component = fixture.componentInstance;
19 | fixture.detectChanges();
20 | });
21 |
22 | it('should create', () => {
23 | expect(component).toBeTruthy();
24 | });
25 | });
26 |
--------------------------------------------------------------------------------
/src/app/remote-instances-list/remote-instances-list.component.ts:
--------------------------------------------------------------------------------
1 | import {Component, OnInit} from '@angular/core';
2 | import {RemoteInstancesListService} from './remote-instances-list.service';
3 | import {RemoteInstance, RemoteInstanceNode, RemoteInstanceService} from '../add-remote-instances/add-remote-instance.service';
4 | import {environment} from '../environment';
5 | import {AddAmazonRDSService} from '../add-amazon-rds/add-amazon-rds.service';
6 |
7 | @Component({
8 | selector: 'app-remote-instances-list',
9 | templateUrl: './remote-instances-list.component.html',
10 | styleUrls: ['./remote-instances-list.component.scss']
11 | })
12 | export class RemoteInstancesListComponent implements OnInit {
13 | public allInstances: RemoteInstance[] = [];
14 | public path: string[] = ['instance']; // same variable as for the loop that generates the table rows
15 | order = 1;
16 | isSorted = false;
17 | isRegion = false;
18 | isDemo = false;
19 | isLoading: boolean;
20 | errorMessage: string;
21 |
22 | constructor(private remoteInstancesListService: RemoteInstancesListService, private awsService: AddAmazonRDSService) {
23 | this.isDemo = environment.demoHosts.includes(location.hostname);
24 | }
25 |
26 | async ngOnInit() {
27 | this.errorMessage = '';
28 | this.isLoading = true;
29 | try {
30 | this.allInstances = await this.remoteInstancesListService.getList();
31 | } catch (err) {
32 | this.errorMessage = err.json().error;
33 | this.isLoading = false;
34 | return;
35 | }
36 | this.errorMessage = this.allInstances === undefined ? 'The list of instances is empty' : '';
37 | this.isLoading = false;
38 | }
39 |
40 | async disableInstance(node: RemoteInstanceNode, service: RemoteInstanceService) {
41 | if (this.isDemo) {
42 | return false;
43 | }
44 | this.isLoading = false;
45 | const text = `Are you sure you want to delete? ${node.name}`;
46 | if (confirm(text)) {
47 | try {
48 | if (service.type !== 'rds') {
49 | const res = await this.remoteInstancesListService.disable(node, service);
50 | } else {
51 | const res = await this.awsService.disable(node);
52 | }
53 | this.allInstances = await this.remoteInstancesListService.getList();
54 | } catch (err) {
55 | this.errorMessage = err.json().error;
56 | this.isLoading = false;
57 | return;
58 | }
59 | this.errorMessage = this.allInstances === undefined ? 'The list of instances is empty' : '';
60 | this.isLoading = false;
61 | }
62 | }
63 |
64 | sortInstances(prop: string) {
65 | this.path = prop.split('.');
66 | this.order = this.order * (-1); // change order
67 | this.isSorted = true;
68 | this.isRegion = prop === 'node.region';
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/app/remote-instances-list/remote-instances-list.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { TestBed, inject } from '@angular/core/testing';
2 |
3 | import { RemoteInstancesListService } from './remote-instances-list.service';
4 |
5 | describe('RemoteInstancesListService', () => {
6 | beforeEach(() => {
7 | TestBed.configureTestingModule({
8 | providers: [RemoteInstancesListService]
9 | });
10 | });
11 |
12 | it('should be created', inject([RemoteInstancesListService], (service: RemoteInstancesListService) => {
13 | expect(service).toBeTruthy();
14 | }));
15 | });
16 |
--------------------------------------------------------------------------------
/src/app/remote-instances-list/remote-instances-list.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core';
2 | import {RemoteInstance, RemoteInstanceNode, RemoteInstanceService} from '../add-remote-instances/add-remote-instance.service';
3 | import {HttpClient, HttpHeaders} from '@angular/common/http';
4 |
5 | @Injectable()
6 | export class RemoteInstancesListService {
7 |
8 | private headers = new HttpHeaders({ 'Content-Type': 'application/json' });
9 |
10 | constructor(private http: HttpClient) {
11 | }
12 |
13 | async getList(): Promise {
14 | const url = `/managed/v0/remote`;
15 | const response = await this.http
16 | .get(url, { headers: this.headers })
17 | .toPromise();
18 | return response['instances'] as RemoteInstance[];
19 | }
20 |
21 | async disable(node: RemoteInstanceNode, service: RemoteInstanceService): Promise<{}> {
22 | const url = `/managed/v0/${service.type}/${node.id}`;
23 | return await this.http
24 | .delete(url, {headers: this.headers})
25 | .toPromise();
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/settings/settings.component.scss:
--------------------------------------------------------------------------------
1 | p {
2 | margin-bottom: 0;
3 | width: 100%;
4 | }
5 | #settings_apply_button {
6 | cursor: pointer;
7 | }
8 |
9 | .accordion-head-second button {
10 | cursor: pointer;
11 | }
12 |
13 | .button-block {
14 | display: flex;
15 | align-items: center;
16 | }
17 |
18 | .button-block .success-message,
19 | .button-block .error-message {
20 | margin-bottom: 0;
21 | margin-left: 15px;
22 | opacity: 0;
23 | transition: .5s ease-in-out all;
24 | }
25 |
26 | .button-block .success-message {
27 | color: green;
28 | }
29 |
30 | .button-block .error-message {
31 | color: red;
32 | }
33 |
34 | .show {
35 | opacity: 1 !important;
36 | }
37 |
38 | .accordion-head-second.text-primary {
39 | display: flex;
40 | align-items: baseline;
41 | }
42 |
43 | #refreshStatusLink,
44 | #refreshLogLink {
45 | color: inherit;
46 | margin-left: 5px;
47 | }
48 |
49 | #update-group {
50 | margin: 0 5px;
51 | }
52 |
53 | .pl-20 {
54 | padding-left: 20px;
55 | }
56 |
57 | .button-block {
58 | height: 40px;
59 | }
60 |
61 | .pmm-button {
62 | width: 95px;
63 | opacity: 1 !important;
64 |
65 | &:disabled {
66 | cursor: not-allowed !important;
67 | opacity: .65;
68 | }
69 | }
70 |
71 | .btn-copied {
72 | opacity: 1 !important;
73 | }
74 |
--------------------------------------------------------------------------------
/src/app/settings/settings.component.spec.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable:no-unused-variable */
2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
3 | import { By } from '@angular/platform-browser';
4 | import { DebugElement } from '@angular/core';
5 |
6 | import { SettingsComponent } from './settings.component';
7 |
8 | describe('SettingsComponent', () => {
9 | let component: SettingsComponent;
10 | let fixture: ComponentFixture;
11 |
12 | beforeEach(async(() => {
13 | TestBed.configureTestingModule({
14 | declarations: [ SettingsComponent ]
15 | })
16 | .compileComponents();
17 | }));
18 |
19 | beforeEach(() => {
20 | fixture = TestBed.createComponent(SettingsComponent);
21 | component = fixture.componentInstance;
22 | fixture.detectChanges();
23 | });
24 |
25 | it('should create', () => {
26 | expect(component).toBeTruthy();
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/src/app/settings/settings.component.ts:
--------------------------------------------------------------------------------
1 | import {Component} from '@angular/core';
2 | import {Router, ActivatedRoute} from '@angular/router';
3 | import {InstanceService} from '../core/instance.service';
4 | import {CoreComponent} from '../core/core.component';
5 | import {environment} from '../environment';
6 | import * as moment from 'moment';
7 | import {SettingsService} from './settings.service';
8 | import {interval, Observable} from 'rxjs';
9 | import {map} from 'rxjs/operators';
10 |
11 | @Component({
12 | moduleId: module.id,
13 | selector: 'app-settings',
14 | templateUrl: './settings.component.html',
15 | styleUrls: ['./settings.component.scss'],
16 | providers: [SettingsService],
17 | })
18 | export class SettingsComponent extends CoreComponent {
19 | public agentStatus: {};
20 | public qanConf: {};
21 | public agentConf: any;
22 | public oldInterval = '1';
23 | public interval = '1';
24 | public collectFrom: 'perfschema' | 'slowlog' = 'slowlog';
25 | public exampleQueries: boolean;
26 | public statusUpdatedFromNow$: Observable;
27 | public logUpdatedFromNow$: Observable;
28 | public agentLog: {};
29 | public severityLeveles: Array = [
30 | 'emerg', 'alert', 'crit', 'err',
31 | 'warning', 'notice', 'info', 'debug'
32 | ];
33 |
34 | public logPeriod = 12;
35 | public isDemo = false;
36 | isSuccess = false;
37 | isError = false;
38 |
39 | constructor(protected route: ActivatedRoute, protected router: Router,
40 | protected settingsService: SettingsService,
41 | protected instanceService: InstanceService) {
42 | super(route, router, instanceService);
43 | this.isDemo = environment.demoHosts.includes(location.hostname);
44 | }
45 |
46 |
47 | /**
48 | * Get agent log for N last hours.
49 | * @param period time period integer in hours
50 | */
51 | setLogPeriod(period): void {
52 | this.logPeriod = period;
53 | this.getAgentLog();
54 | }
55 |
56 | /**
57 | * get fresh agent log
58 | */
59 | refreshAgentLog(): void {
60 | this.getAgentLog();
61 | }
62 |
63 | validateValue(value) {
64 | if (this.interval === null) { return this.interval = ''; }
65 |
66 | if (this.oldInterval !== value) {
67 | this.interval = (value > 60 || value < 1) ? this.oldInterval : value;
68 | }
69 | this.oldInterval = this.interval;
70 | }
71 |
72 | /**
73 | * Get from agent:
74 | * - Collect interval: positive intager;
75 | * - Send real query examples: boolean;
76 | * - Collect from: 'slowlog' or 'perfschema'.
77 | */
78 | public async getAgentDefaults() {
79 | const res = await this.settingsService.getAgentDefaults(this.agent.UUID, this.dbServer.UUID);
80 | try {
81 | this.agentConf = res;
82 | this.interval = (this.agentConf.qan.Interval / 60).toString();
83 | this.collectFrom = this.agentConf.qan.CollectFrom;
84 | this.exampleQueries = this.agentConf.qan.ExampleQueries;
85 | } catch (err) {
86 | console.error(err)
87 | }
88 | }
89 |
90 | /**
91 | * Set on agent:
92 | * - Collect interval: positive intager;
93 | * - Send real query examples: boolean;
94 | * - Collect from: 'slowlog' or 'perfschema'.
95 | */
96 | public async setAgentDefaults() {
97 | const res = await this.settingsService.setAgentDefaults(
98 | this.agent.UUID,
99 | this.dbServer.UUID,
100 | +this.interval,
101 | this.exampleQueries,
102 | this.collectFrom
103 | );
104 | const visibleMessageTime = 5000;
105 | try {
106 | // this.agentConf = res; // diffrent responce than GetDefaults.
107 | this.isSuccess = true;
108 | this.isError = false;
109 | setTimeout(() => {
110 | this.isSuccess = false;
111 | this.isError = false;
112 | }, visibleMessageTime); // add const
113 | this.getAgentDefaults();
114 | } catch (err) {
115 | this.isSuccess = false;
116 | this.isError = true;
117 | setTimeout(() => {
118 | this.isSuccess = false;
119 | this.isError = false;
120 | }, visibleMessageTime);
121 | console.error(err);
122 | }
123 | }
124 |
125 | /**
126 | * Get slice of exported variables of agent.
127 | */
128 | getAgentStatus() {
129 | this.agentStatus = this.settingsService.getAgentStatus(this.agent.UUID);
130 | const updated: any = moment();
131 | this.statusUpdatedFromNow$ = interval(60000).pipe(map(n => updated.fromNow()));
132 | }
133 |
134 | /**
135 | * get agent log for some period.
136 | */
137 | getAgentLog() {
138 | const begin = moment.utc().subtract(this.logPeriod, 'h').format('YYYY-MM-DDTHH:mm:ss');
139 | const end = moment.utc().format('YYYY-MM-DDTHH:mm:ss');
140 | this.agentLog = this.settingsService.getAgentLog(this.agent.UUID, begin, end);
141 | const updated: any = moment();
142 | this.logUpdatedFromNow$ = interval(60000).pipe(map(n => updated.fromNow()));
143 | }
144 |
145 | /**
146 | * Ovverrides parent method.
147 | * Executes on route was changed to refresh data.
148 | * @param params - URL query parameters
149 | */
150 | onChangeParams(params) {
151 | this.getAgentDefaults();
152 | this.getAgentStatus();
153 | this.getAgentLog();
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/src/app/settings/settings.service.spec.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable:no-unused-variable */
2 |
3 | import { TestBed, async, inject } from '@angular/core/testing';
4 | import { SettingsService } from './settings.service';
5 |
6 | describe('SettingsService', () => {
7 | beforeEach(() => {
8 | TestBed.configureTestingModule({
9 | providers: [SettingsService]
10 | });
11 | });
12 |
13 | it('should ...', inject([SettingsService], (service: SettingsService) => {
14 | expect(service).toBeTruthy();
15 | }));
16 | });
17 |
--------------------------------------------------------------------------------
/src/app/settings/settings.service.ts:
--------------------------------------------------------------------------------
1 | import {Injectable} from '@angular/core';
2 | import {HttpClient, HttpHeaders, HttpParams} from '@angular/common/http';
3 |
4 | export type CollectFrom = 'slowlog' | 'perfschema';
5 |
6 | @Injectable()
7 | export class SettingsService {
8 |
9 | private headers = new HttpHeaders({ 'Content-Type': 'application/json' });
10 |
11 | constructor(private httpClient: HttpClient) { }
12 |
13 | public async getAgentStatus(agentUUID: string): Promise<{}> {
14 | const url = `/qan-api/agents/${agentUUID}/status`;
15 |
16 | const response = await this.httpClient
17 | .get(url, { headers: this.headers })
18 | .toPromise();
19 | return response as {};
20 | }
21 |
22 | public async getAgentLog(agentUUID, begin, end: string): Promise<{}> {
23 | const url = `/qan-api/agents/${agentUUID}/log`;
24 |
25 | const params = new HttpParams()
26 | .set('begin', begin)
27 | .set('end', end);
28 | const response = await this.httpClient
29 | .get(url, { headers: this.headers, params: params })
30 | .toPromise();
31 | return response as {};
32 | }
33 |
34 | public async getAgentDefaults(agentUUID: string, dbServerUUID: string): Promise<{}> {
35 | const url = `/qan-api/agents/${agentUUID}/cmd`;
36 | const params = {
37 | AgentUUID: agentUUID,
38 | Service: 'agent',
39 | Cmd: 'GetDefaults',
40 | Data: btoa(JSON.stringify({ UUID: dbServerUUID }))
41 | };
42 |
43 | const resp = await this.httpClient
44 | .put(url, params, {headers: this.headers})
45 | .toPromise();
46 | if (!!resp['Error']) {
47 | throw new Error(resp['Error']);
48 | } else {
49 | return JSON.parse(atob(resp['Data']));
50 | }
51 | }
52 |
53 | public async setAgentDefaults(agentUUID: string, dbServerUUID: string, interval: number,
54 | exampleQueries: boolean, collectFrom: CollectFrom): Promise<{}> {
55 | const url = `/qan-api/agents/${agentUUID}/cmd`;
56 |
57 | const data = {
58 | UUID: dbServerUUID,
59 | Interval: interval * 60,
60 | ExampleQueries: exampleQueries,
61 | CollectFrom: collectFrom
62 | };
63 |
64 | const params = {
65 | AgentUUID: agentUUID,
66 | Service: 'qan',
67 | Cmd: 'RestartTool',
68 | Data: btoa(JSON.stringify(data))
69 | };
70 |
71 | const resp = await this.httpClient
72 | .put(url, params, {headers: this.headers})
73 | .toPromise();
74 | if (!!resp['Error']) {
75 | throw new Error(resp['Error']);
76 | } else {
77 | return JSON.parse(atob(resp['Data']));
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/app/shared/humanize.pipe.spec.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable:no-unused-variable */
2 |
3 | import { TestBed, async } from '@angular/core/testing';
4 | import { HumanizePipe } from './humanize.pipe';
5 |
6 | describe('HumanizePipe', () => {
7 | it('create an instance', () => {
8 | const pipe = new HumanizePipe();
9 | expect(pipe).toBeTruthy();
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/src/app/shared/humanize.pipe.ts:
--------------------------------------------------------------------------------
1 | import { Pipe, PipeTransform } from '@angular/core';
2 | import * as moment from 'moment';
3 | import * as numeral from 'numeral';
4 |
5 | /**
6 | * @desc humanize time duration
7 | * @example {{ duration | humanize }}
8 | */
9 | @Pipe({ name: 'humanize' })
10 | export class HumanizePipe implements PipeTransform {
11 |
12 | parceTime(input: number) {
13 |
14 | let dur = '';
15 | const dur_sec = moment.duration(input, 's');
16 | switch (true) {
17 | case input === 0:
18 | dur = '0';
19 | break;
20 | case dur_sec.as('s') > 1 && dur_sec.as('s') < 60:
21 | dur = dur_sec.as('s').toFixed(2) + ' sec';
22 | break;
23 | case dur_sec.as('s') >= 60:
24 | let secs = dur_sec.as('s');
25 | const secondsInDay = 24 * 60 * 60;
26 | if (secs >= secondsInDay) {
27 | const days = Math.floor(secs / secondsInDay);
28 | dur = `${days} days, `;
29 | secs = secs % secondsInDay;
30 | }
31 | dur += numeral(secs).format('00:00:00');
32 | break;
33 | case dur_sec.as('ms') < 1:
34 | dur = (dur_sec.as('ms') * 1000).toFixed(2) + ' \µs';
35 | break;
36 | default:
37 | dur = dur_sec.as('ms').toFixed(2) + ' ms';
38 | break;
39 | }
40 | return dur;
41 | }
42 |
43 | transform(input: number, name: string): string {
44 |
45 | if (input === null) {
46 | return '0';
47 | }
48 |
49 | let res = '0';
50 | switch (true) {
51 | // "top 10"/profile queries no name parameters
52 | case name === undefined:
53 | res = this.parceTime(input);
54 | break;
55 | // time
56 | case name.indexOf('time') > -1:
57 | res = (input !== 0 && input < 0.00001) ? '<' : '';
58 | res += this.parceTime(input);
59 | break;
60 | // size
61 | case name.indexOf('size') > -1:
62 | if (input !== 0 && input < 0.01) {
63 | res = '<0.01 B';
64 | } else {
65 | res = numeral(input).format('0.00 b');
66 | }
67 | res = res.replace(/([\d]) B/, '$1 Bytes');
68 | break;
69 | // ops
70 | case name.indexOf('number') > -1:
71 | if (input !== 0 && input < 0.01) {
72 | res = '<0.01';
73 | } else {
74 | res = numeral(input).format('0.00a');
75 | }
76 | break;
77 | case name.indexOf('percent') > -1:
78 | if (input !== 0 && input < 0.0001) {
79 | res = '<0.01';
80 | } else {
81 | res = numeral(input).format('0.00%');
82 | }
83 | break;
84 | // ops
85 | default:
86 | if (input !== 0 && input < 0.01) {
87 | res = '<0.01';
88 | } else {
89 | res = numeral(input).format('0.00 a');
90 | }
91 | break;
92 | }
93 | return String(res).replace('<0.00', '<0.01');
94 |
95 | }
96 | }
97 |
--------------------------------------------------------------------------------
/src/app/shared/latency-chart.directive.spec.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable:no-unused-variable */
2 |
3 | import { TestBed, async } from '@angular/core/testing';
4 | import { ElementRef } from '@angular/core';
5 | import { LatencyChartDirective } from './latency-chart.directive';
6 |
7 | describe('LatencyChartDirective', () => {
8 | it('should create an instance', () => {
9 | const elementRef = new ElementRef('
');
10 | const directive = new LatencyChartDirective(elementRef);
11 | expect(directive).toBeTruthy();
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/src/app/shared/latency-chart.directive.ts:
--------------------------------------------------------------------------------
1 | import { Directive, Input, HostBinding } from '@angular/core';
2 | import { ElementRef } from '@angular/core';
3 |
4 | import { select } from 'd3-selection';
5 | import { scaleLog } from 'd3-scale';
6 |
7 | import { HumanizePipe } from './humanize.pipe';
8 |
9 | @Directive({
10 | selector: '[appLatencyChart]'
11 | })
12 | export class LatencyChartDirective {
13 |
14 | @HostBinding('attr.data-tooltip')
15 | @Input() dataTooltip: string;
16 | @Input() measurement = 'time';
17 | @Input() metricPrefix = '';
18 |
19 | @Input() set appLatencyChart(data: {}) {
20 | if (data !== null) {
21 | this.drawChart(data);
22 | }
23 | }
24 |
25 | constructor(
26 | public elementRef: ElementRef,
27 | ) { }
28 |
29 | drawChart(data: {}) {
30 | const chart = select(this.elementRef.nativeElement);
31 | chart.selectAll('*').remove();
32 | const svg: any = chart.append('svg')
33 | .attr('height', '20')
34 | .attr('width', '100')
35 | .attr('class', 'scaling-svg')
36 | .attr('viewBox', '0 0 100 20');
37 |
38 | const width = Math.floor(svg.node().getBoundingClientRect().width);
39 | svg.attr('width', width).attr('viewBox', '0 0 ' + width + ' 20');
40 |
41 | const x = scaleLog()
42 | .domain([0.00001, 10000])
43 | .range([2, width - 2])
44 | .clamp(true)
45 | .nice();
46 |
47 | let min = 0;
48 | let max = 0;
49 | let avg = 0;
50 | let p95 = 0;
51 |
52 | if (!!this.metricPrefix) {
53 | min = `${this.metricPrefix}_min` in data ? data[`${this.metricPrefix}_min`] : 0;
54 | max = `${this.metricPrefix}_max` in data ? data[`${this.metricPrefix}_max`] : 0;
55 | avg = `${this.metricPrefix}_avg` in data ? data[`${this.metricPrefix}_avg`] : 0;
56 | p95 = `${this.metricPrefix}_p95` in data ? data[`${this.metricPrefix}_p95`] : 0;
57 | } else {
58 | min = 'Min' in data ? data['Min'] : 0;
59 | max = 'Max' in data ? data['Max'] : 0;
60 | avg = 'Avg' in data ? data['Avg'] : 0;
61 | p95 = 'P95' in data ? data['P95'] : 0;
62 | }
63 |
64 | const humanize = new HumanizePipe();
65 | let tooltip = ` ⌜ Min: ${humanize.transform(min, this.measurement)}
66 | ⌟ Max: ${humanize.transform(max, this.measurement)}
67 | ◦ Avg: ${humanize.transform(avg, this.measurement)}`;
68 |
69 | if (p95 !== 0 && p95 !== null ) {
70 | tooltip += `
71 | • 95%: ${humanize.transform(p95, this.measurement)}`;
72 | }
73 | this.dataTooltip = tooltip;
74 |
75 | const g = svg.append('g');
76 |
77 | // hrAxes
78 | g.append('line')
79 | .attr('class', 'latency-chart-x')
80 | .attr('x1', '0')
81 | .attr('stroke-dasharray', '1, 1')
82 | .attr('y1', '13px')
83 | .attr('x2', width)
84 | .attr('y2', '13px');
85 |
86 | // hrLine
87 | g.append('line')
88 | .attr('class', 'latency-chart-line')
89 | .attr('x1', x(min) + '')
90 | .attr('y1', '13px')
91 | .attr('x2', x(max) + '')
92 | .attr('y2', '13px');
93 |
94 | // minMark
95 | g.append('line')
96 | .attr('class', 'latency-chart-min')
97 | .attr('x1', x(min) + '')
98 | .attr('y1', '13px')
99 | .attr('x2', x(min) + '')
100 | .attr('y2', '19px');
101 |
102 | // maxMark
103 | g.append('line')
104 | .attr('class', 'latency-chart-max')
105 | .attr('x1', x(max) + '')
106 | .attr('y1', '8px')
107 | .attr('x2', x(max) + '')
108 | .attr('y2', '13px');
109 |
110 | // avgMark
111 | g.append('circle')
112 | .attr('class', 'latency-chart-avg')
113 | .attr('r', 3)
114 | .attr('cx', x(avg) + '')
115 | .attr('cy', '13px');
116 |
117 | // p95Mark
118 | if (p95 > 0) {
119 | g.append('circle')
120 | .attr('class', 'latency-chart-p95')
121 | .attr('r', 2)
122 | .attr('cx', x(p95) + '')
123 | .attr('cy', '13px');
124 | }
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/src/app/shared/load-sparklines.directive.spec.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable:no-unused-variable */
2 |
3 | import { TestBed, async } from '@angular/core/testing';
4 | import { ElementRef } from '@angular/core';
5 | import { LoadSparklinesDirective } from './load-sparklines.directive';
6 |
7 | describe('LoadSparklinesDirective', () => {
8 | it('should create an instance', () => {
9 | const elementRef = new ElementRef('
');
10 | const directive = new LoadSparklinesDirective(elementRef);
11 | expect(directive).toBeTruthy();
12 | });
13 | });
14 |
--------------------------------------------------------------------------------
/src/app/shared/load-sparklines.directive.ts:
--------------------------------------------------------------------------------
1 | import { Directive, Input, HostBinding } from '@angular/core';
2 | import { ElementRef } from '@angular/core';
3 |
4 | import * as moment from 'moment';
5 | import { select } from 'd3-selection';
6 | import { scaleLinear, scaleTime } from 'd3-scale';
7 | import { isoParse, utcFormat, extent, line, area, bisector } from 'd3';
8 | import { event as currentEvent, mouse } from 'd3-selection';
9 |
10 | import { HumanizePipe } from './humanize.pipe';
11 | import { MomentFormatPipe } from './moment-format.pipe';
12 |
13 | /**
14 | * Display sparklines in top queries and metrics.
15 | */
16 | @Directive({ selector: '[appLoadSparklines]' })
17 | export class LoadSparklinesDirective {
18 |
19 | protected _xkey: string;
20 | protected _ykey: string;
21 | protected _measurement: string;
22 |
23 | humanize = new HumanizePipe();
24 | dateFormat = new MomentFormatPipe();
25 |
26 | @HostBinding('attr.data-tooltip')
27 | @Input() dataTooltip: string;
28 |
29 | constructor(
30 | public elementRef: ElementRef,
31 | ) { }
32 |
33 | @Input() set xkey(xkey: string) {
34 | this._xkey = xkey;
35 | }
36 |
37 | @Input() set ykey(ykey: string) {
38 | this._ykey = ykey;
39 | }
40 |
41 | @Input() set measurement(measurement: string) {
42 | this._measurement = measurement;
43 | }
44 |
45 | @Input() set appLoadSparklines(data: Array<{}>) {
46 | if (data !== null) {
47 | setTimeout(() => {
48 | this.drawChart(data)
49 | }, 1);
50 | }
51 | }
52 |
53 | drawChart(data: Array<{}>) {
54 | const xkey = this._xkey;
55 | const ykey = this._ykey;
56 | const measurement = this._measurement || 'number';
57 |
58 | const chart = select(this.elementRef.nativeElement);
59 | chart.selectAll('*').remove();
60 | const svg: any = chart.append('svg')
61 | .attr('height', '20')
62 | .attr('width', '100')
63 | .attr('class', 'scaling-svg')
64 | .attr('preserveAspectRatio', 'none')
65 | .attr('viewBox', '-1 0 102 20');
66 |
67 | const height = 15;
68 | const width = Math.floor(svg.node().getBoundingClientRect().width);
69 | svg.attr('width', width).attr('viewBox', '-1 0 ' + (width + 2) + ' 20');
70 |
71 | const xDomain = extent(data.map(d => moment.utc(d[xkey])));
72 |
73 | const xScale = scaleTime().range([2, width - 2]).domain(xDomain);
74 |
75 | const yDomain = extent(data.map(d => ykey in d ? d[ykey] : 0));
76 |
77 | const yScale = scaleLinear().range([height, 2]).domain(yDomain).clamp(true);
78 |
79 | const svgLine = line()
80 | .defined(d => !d['NoData'])
81 | .x(d => xScale(moment.utc(d[xkey])))
82 | .y(d => yScale(d[ykey] === undefined ? 0 : d[ykey]));
83 |
84 | const svgArea = area()
85 | .defined(d => !d['NoData'])
86 | .x(d => xScale(moment.utc(d[xkey])))
87 | .y0(d => yScale(d[ykey] === undefined ? 0 : d[ykey]))
88 | .y1(height - 1);
89 |
90 | const g = svg.append('g').attr('transform', 'translate(0, 0)');
91 |
92 | g.append('path')
93 | .datum(data)
94 | .attr('class', 'area')
95 | .attr('d', svgArea);
96 |
97 | g.append('path')
98 | .datum(data)
99 | .attr('class', 'line')
100 | .attr('d', svgLine);
101 |
102 | g.append('line')
103 | .attr('x1', width + 20)
104 | .attr('y1', height)
105 | .attr('x2', '0')
106 | .attr('y2', height)
107 | .attr('class', 'x-axis');
108 |
109 | const focus = g.append('g').style('display', 'none');
110 |
111 | focus.append('line')
112 | .attr('id', 'focusLineX')
113 | .attr('class', 'focusLine');
114 |
115 | focus.append('circle')
116 | .attr('id', 'focusCircle')
117 | .attr('r', 1.5)
118 | .attr('class', 'circle focusCircle');
119 |
120 | focus.append('text')
121 | .attr('id', 'focusText')
122 | .attr('font-size', '10')
123 | .attr('x', 1)
124 | .attr('y', 8);
125 |
126 | // @ts-ignore TS2345
127 | const bisectDate = bisector((d, x) => moment.utc(d[xkey]).isBefore(x)).right;
128 |
129 | const rect = g.append('rect')
130 | .attr('class', 'overlay')
131 | .attr('width', width)
132 | .attr('height', height)
133 | .on('mouseover', () => focus.style('display', null))
134 | .on('mouseout', () => focus.style('display', 'none'));
135 |
136 | rect.on('mousemove', (p, e) => {
137 | const coords = mouse(currentEvent.currentTarget);
138 |
139 | const mouseDate: any = moment.utc(xScale.invert(coords[0]));
140 | // returns the index to the current data item
141 | const i = Math.min(Math.max(bisectDate(data, mouseDate), 0), data.length - 1);
142 | let d = data[i];
143 |
144 | // correction bisector to use data[0] on right edge of sparkline.
145 | if (i === 1) {
146 | const d0 = moment.utc(data[0][xkey]);
147 | const d1 = moment.utc(data[1][xkey]);
148 | if (mouseDate.diff(d1) > 0 && d0.diff(mouseDate) < mouseDate.diff(d1)) {
149 | d = data[0];
150 | }
151 | }
152 |
153 | const x = xScale(isoParse(d[xkey]));
154 | const y = yScale(d[ykey] === undefined ? 0 : d[ykey]);
155 |
156 | const MIN = 0,
157 | MAX = 1;
158 | focus.select('#focusCircle')
159 | .attr('cx', x)
160 | .attr('cy', y);
161 | focus.select('#focusLineX')
162 | .attr('x1', x).attr('y1', yScale(yDomain[MIN]))
163 | .attr('x2', x).attr('y2', yScale(yDomain[MAX]));
164 |
165 | const value = d[ykey] === undefined ? 0 : d[ykey];
166 | const load = this.humanize.transform(value, measurement);
167 |
168 | const dateToShow = this.dateFormat.transform(moment(d[xkey]).utc());
169 | this.dataTooltip = d['NoData'] ? `No data at ${dateToShow}` : `${load} at ${dateToShow}`;
170 | });
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/src/app/shared/map-to-iterable.pipe.spec.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable:no-unused-variable */
2 |
3 | import { TestBed, async } from '@angular/core/testing';
4 | import { MapToIterablePipe } from './map-to-iterable.pipe';
5 |
6 | describe('MapToIterablePipe', () => {
7 | it('create an instance', () => {
8 | const pipe = new MapToIterablePipe();
9 | expect(pipe).toBeTruthy();
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/src/app/shared/map-to-iterable.pipe.ts:
--------------------------------------------------------------------------------
1 | import { Pipe, PipeTransform } from '@angular/core';
2 |
3 |
4 | @Pipe({
5 | name: 'mapToIterable'
6 | })
7 | export class MapToIterablePipe implements PipeTransform {
8 | transform(dict: Object): Array<{}> {
9 | if (dict === null) {
10 | return null;
11 | }
12 | const a: Array<{}> = [];
13 | for (const key in dict) {
14 | if (dict.hasOwnProperty(key)) {
15 | a.push({key: key, val: dict[key]});
16 | }
17 | }
18 | return a;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/app/shared/moment-format.pipe.spec.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable:no-unused-variable */
2 |
3 | import { TestBed, async } from '@angular/core/testing';
4 | import { MomentFormatPipe } from './moment-format.pipe';
5 |
6 | describe('MomentFormatPipe', () => {
7 | it('create an instance', () => {
8 | const pipe = new MomentFormatPipe();
9 | expect(pipe).toBeTruthy();
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/src/app/shared/moment-format.pipe.ts:
--------------------------------------------------------------------------------
1 | import { Pipe, PipeTransform } from '@angular/core';
2 | import * as moment from 'moment';
3 |
4 | @Pipe({ name: 'dateFormat' })
5 | export class MomentFormatPipe implements PipeTransform {
6 |
7 | timezone = 'browser';
8 | constructor () {
9 | this.timezone = this.getCookie('timezone') || 'browser';
10 | }
11 |
12 | getCookie(name) {
13 | return document.cookie.split('; ').reduce((r, v) => {
14 | const parts = v.split('=');
15 | return parts[0] === name ? decodeURIComponent(parts[1]) : r;
16 | }, '');
17 | }
18 |
19 | transform(value: any, format = 'YYYY-MM-DD HH:mm:ss'): string {
20 | if (value === null) {
21 | return null;
22 | }
23 |
24 | if (this.timezone === 'browser') {
25 | return value.local().format(format);
26 | } else {
27 | return value.format(format);
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/app/shared/parse-query-param-date.pipe.spec.ts:
--------------------------------------------------------------------------------
1 | import { ParseQueryParamDatePipe } from './parse-query-param-date.pipe';
2 |
3 | describe('ParseQueryParamDatePipe', () => {
4 | it('create an instance', () => {
5 | const pipe = new ParseQueryParamDatePipe();
6 | expect(pipe).toBeTruthy();
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/src/app/shared/parse-query-param-date.pipe.ts:
--------------------------------------------------------------------------------
1 | import { Pipe, PipeTransform } from '@angular/core';
2 | import { MomentFormatPipe } from './moment-format.pipe';
3 | import * as moment from 'moment';
4 |
5 | type TimeEdge = 'from' | 'to';
6 |
7 | /**
8 | *
9 | * var spans = {
10 | * 's': {display: 'second'},
11 | * 'm': {display: 'minute'},
12 | * 'h': {display: 'hour'},
13 | * 'd': {display: 'day'},
14 | * 'w': {display: 'week'},
15 | * 'M': {display: 'month'},
16 | * 'y': {display: 'year'},
17 | * };
18 | *
19 | * var rangeOptions = [
20 | * { from: 'now/d', to: 'now/d', display: 'Today', section: 2 },
21 | * { from: 'now/d', to: 'now', display: 'Today so far', section: 2 },
22 | * { from: 'now/w', to: 'now/w', display: 'This week', section: 2 },
23 | * { from: 'now/w', to: 'now', display: 'This week so far', section: 2 },
24 | * { from: 'now/M', to: 'now/M', display: 'This month', section: 2 },
25 | * { from: 'now/y', to: 'now/y', display: 'This year', section: 2 },
26 | *
27 | * { from: 'now-1d/d', to: 'now-1d/d', display: 'Yesterday', section: 1 },
28 | * { from: 'now-2d/d', to: 'now-2d/d', display: 'Day before yesterday', section: 1 },
29 | * { from: 'now-7d/d', to: 'now-7d/d', display: 'This day last week', section: 1 },
30 | * { from: 'now-1w/w', to: 'now-1w/w', display: 'Previous week', section: 1 },
31 | * { from: 'now-1M/M', to: 'now-1M/M', display: 'Previous month', section: 1 },
32 | * { from: 'now-1y/y', to: 'now-1y/y', display: 'Previous year', section: 1 },
33 | *
34 | * { from: 'now-5m', to: 'now', display: 'Last 5 minutes', section: 3 },
35 | * { from: 'now-15m', to: 'now', display: 'Last 15 minutes', section: 3 },
36 | * { from: 'now-30m', to: 'now', display: 'Last 30 minutes', section: 3 },
37 | * { from: 'now-1h', to: 'now', display: 'Last 1 hour', section: 3 },
38 | * { from: 'now-3h', to: 'now', display: 'Last 3 hours', section: 3 },
39 | * { from: 'now-6h', to: 'now', display: 'Last 6 hours', section: 3 },
40 | * { from: 'now-12h', to: 'now', display: 'Last 12 hours', section: 3 },
41 | * { from: 'now-24h', to: 'now', display: 'Last 24 hours', section: 3 },
42 | *
43 | * { from: 'now-2d', to: 'now', display: 'Last 2 days', section: 0 },
44 | * { from: 'now-7d', to: 'now', display: 'Last 7 days', section: 0 },
45 | * { from: 'now-30d', to: 'now', display: 'Last 30 days', section: 0 },
46 | * { from: 'now-90d', to: 'now', display: 'Last 90 days', section: 0 },
47 | * { from: 'now-6M', to: 'now', display: 'Last 6 months', section: 0 },
48 | * { from: 'now-1y', to: 'now', display: 'Last 1 year', section: 0 },
49 | * { from: 'now-2y', to: 'now', display: 'Last 2 years', section: 0 },
50 | * { from: 'now-5y', to: 'now', display: 'Last 5 years', section: 0 },
51 | * ];from=now-7d%2Fd&to=now-7d%2Fd
52 | */
53 | @Pipe({
54 | name: 'parseQueryParamDate'
55 | })
56 | export class ParseQueryParamDatePipe implements PipeTransform {
57 |
58 | transform(date: string, edge: TimeEdge) {
59 | const momentFormatPipe = new MomentFormatPipe();
60 | const nowFunc = momentFormatPipe.timezone === 'utc' ? moment.utc : moment;
61 | let parsedDate;
62 | // from=now
63 |
64 | if (date === undefined && edge === 'from') {
65 | return nowFunc().subtract(1, 'h');
66 | }
67 | if (date === undefined && edge === 'to') {
68 | return nowFunc();
69 | }
70 |
71 | if (date === 'now') {
72 | return nowFunc();
73 | }
74 | // from=now-5d&to=now-6M ... from=now/w&to=now/w
75 | if (date.length > 4 && date.startsWith('now')) {
76 | // let subtrahend = date.substr(4);
77 | // ex: ["now-7d/d", "now", "-", "7", "d", "/", "d"]
78 | const parts = date.match('(now)(-|/)?([0-9]*)([yMwdhms])(/)?([yMwdhms])?');
79 |
80 | if (parts[1] === 'now') {
81 | parsedDate = nowFunc();
82 | }
83 | if (parts[2] === '-') {
84 | parsedDate.subtract(parts[3], parts[4]);
85 | }
86 | if (parts[2] === '/') {
87 | if (edge === 'from') {
88 | return parsedDate.startOf(parts[4]);
89 | } else {
90 | return parsedDate.endOf(parts[4]);
91 | }
92 | }
93 | if (parts.length > 4 && parts[5] === '/') {
94 | if (edge === 'from') {
95 | return parsedDate.startOf(parts[6]);
96 | } else {
97 | return parsedDate.endOf(parts[6]);
98 | }
99 | }
100 | } else {
101 | // expect unix timestamp in milliseconds
102 | const isnum = /^\d+$/.test(date);
103 | if (isnum) {
104 | return nowFunc(parseInt(date, 10));
105 | } else {
106 | return nowFunc(date);
107 | }
108 | }
109 | return parsedDate;
110 | }
111 |
112 | }
113 |
--------------------------------------------------------------------------------
/src/app/shared/shared.module.ts:
--------------------------------------------------------------------------------
1 | import {NgModule} from '@angular/core';
2 | import {CommonModule} from '@angular/common';
3 | import {FormsModule} from '@angular/forms';
4 | import {NgbModule} from '@ng-bootstrap/ng-bootstrap';
5 | import {RouterModule} from '@angular/router';
6 | import {HumanizePipe} from './humanize.pipe';
7 | import {LatencyChartDirective} from './latency-chart.directive';
8 | import {LoadSparklinesDirective} from './load-sparklines.directive';
9 | import {MapToIterablePipe} from './map-to-iterable.pipe';
10 | import {MomentFormatPipe} from './moment-format.pipe';
11 | import {TruncateRootPipe} from './truncate-root.pipe';
12 | import {ParseQueryParamDatePipe} from './parse-query-param-date.pipe';
13 | import {SortingTablePipe} from './sorting-table.pipe';
14 | import {HttpClientModule} from '@angular/common/http';
15 |
16 | @NgModule({
17 | imports: [
18 | CommonModule
19 | ],
20 | declarations: [
21 | HumanizePipe,
22 | LatencyChartDirective,
23 | LoadSparklinesDirective,
24 | MapToIterablePipe,
25 | MomentFormatPipe,
26 | TruncateRootPipe,
27 | ParseQueryParamDatePipe,
28 | SortingTablePipe
29 | ],
30 | exports: [
31 | MapToIterablePipe,
32 | MomentFormatPipe,
33 | TruncateRootPipe,
34 | HumanizePipe,
35 | LatencyChartDirective,
36 | LoadSparklinesDirective,
37 | SortingTablePipe,
38 | CommonModule,
39 | FormsModule,
40 | HttpClientModule,
41 | NgbModule,
42 | RouterModule
43 | ]
44 | })
45 | export class SharedModule {
46 | static forRoot() {
47 | return {
48 | ngModule: SharedModule,
49 | providers: [],
50 | };
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/app/shared/sorting-table.pipe.spec.ts:
--------------------------------------------------------------------------------
1 | import { SortingTablePipe } from './sorting-table.pipe';
2 |
3 | describe('sortingInstancesPipe', () => {
4 | it('create an instance', () => {
5 | const pipe = new SortingTablePipe();
6 | expect(pipe).toBeTruthy();
7 | });
8 | });
9 |
--------------------------------------------------------------------------------
/src/app/shared/sorting-table.pipe.ts:
--------------------------------------------------------------------------------
1 | import {Pipe, PipeTransform} from '@angular/core';
2 |
3 | @Pipe({name: 'sortingTable'})
4 | export class SortingTablePipe implements PipeTransform {
5 |
6 | transform(instance: any[], path: string[], order: number = 1): any[] {
7 | // Check if is not null
8 | if (!instance || !path || !order) {
9 | return instance;
10 | }
11 |
12 | return instance.sort((a: any, b: any) => {
13 | // We go for each property followed by path
14 | path.forEach(property => {
15 | a = a[property];
16 | b = b[property];
17 | });
18 |
19 | // Order * (-1): We change our order
20 | return a > b ? order : order * (-1);
21 | })
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/src/app/shared/truncate-root.pipe.spec.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable:no-unused-variable */
2 |
3 | import { TestBed, async } from '@angular/core/testing';
4 | import { TruncateRootPipe } from './truncate-root.pipe';
5 |
6 | describe('TruncateRootPipe', () => {
7 | it('create an instance', () => {
8 | const pipe = new TruncateRootPipe();
9 | expect(pipe).toBeTruthy();
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/src/app/shared/truncate-root.pipe.ts:
--------------------------------------------------------------------------------
1 | import { Pipe, PipeTransform } from '@angular/core';
2 |
3 | @Pipe({ name: 'truncateRoot' })
4 | export class TruncateRootPipe implements PipeTransform {
5 | transform(input: string, len: number): string {
6 | if (input === null) {
7 | return null;
8 | }
9 | if (input.length > len && len > 4) {
10 | let res = '';
11 | if (len % 2 === 0) {
12 | const half = len / 2;
13 | res = input.substring(0, half - 1);
14 | res += '...';
15 | res += input.substring(input.length - half + 2, input.length);
16 | } else {
17 | const half = Math.floor(len / 2);
18 | res = input.substring(0, half - 1);
19 | res += '...';
20 | res += input.substring(input.length - half + 1, input.length);
21 | }
22 | return res;
23 | } else {
24 | return input;
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/app/summary/summary.component.html:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 | Download Summary
6 |
7 |
8 | Host name of All is not valid, please click the Host dropdown and select a valid host.
9 | Host name is not valid, please click the Host dropdown and select a valid host. If the Host list is empty, use
10 |
pmm-admin add
to add a monitoring service and check again. For more information on how to add a monitoring service,
11 | consult
12 |
14 | PMM documentation.
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
System Summary
24 |
25 |
26 |
27 | {{ serverSummary }}
28 |
29 |
30 |
31 | {{ serverSummaryError }}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
MySQL Summary
39 |
40 |
41 |
42 | {{ mysqlSummary }}
43 |
44 |
45 |
46 | {{ mysqlSummaryError }}
47 |
48 |
49 |
50 |
51 |
52 |
53 |
MongoDB Summary
54 |
55 |
56 |
57 | {{ mongoSummary }}
58 |
59 |
60 |
61 | {{ mongoSummaryError }}
62 |
63 |
64 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/src/app/summary/summary.component.scss:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/percona/qan-app/dc15e33d41a0b87abec08abb83aded5b0f83356a/src/app/summary/summary.component.scss
--------------------------------------------------------------------------------
/src/app/summary/summary.component.spec.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable:no-unused-variable */
2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing';
3 | import { By } from '@angular/platform-browser';
4 | import { DebugElement } from '@angular/core';
5 |
6 | import { SummaryComponent } from './summary.component';
7 |
8 | describe('SummaryComponent', () => {
9 | let component: SummaryComponent;
10 | let fixture: ComponentFixture;
11 |
12 | beforeEach(async(() => {
13 | TestBed.configureTestingModule({
14 | declarations: [ SummaryComponent ]
15 | })
16 | .compileComponents();
17 | }));
18 |
19 | beforeEach(() => {
20 | fixture = TestBed.createComponent(SummaryComponent);
21 | component = fixture.componentInstance;
22 | fixture.detectChanges();
23 | });
24 |
25 | it('should create', () => {
26 | expect(component).toBeTruthy();
27 | });
28 | });
29 |
--------------------------------------------------------------------------------
/src/app/summary/summary.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { Router, ActivatedRoute, Params } from '@angular/router';
3 |
4 | import { CoreComponent } from '../core/core.component';
5 | import { SummaryService } from './summary.service';
6 | import { Instance, InstanceService } from '../core/instance.service';
7 | import * as JSZip from 'jszip';
8 | import saveAs from 'jszip/vendor/FileSaver';
9 | import * as moment from 'moment';
10 | import { MomentFormatPipe } from '../shared/moment-format.pipe';
11 |
12 |
13 | /**
14 | * Shows MySQL and Server Summary
15 | */
16 | @Component({
17 | moduleId: module.id,
18 | selector: 'app-summary',
19 | templateUrl: './summary.component.html',
20 | styleUrls: ['./summary.component.scss']
21 | })
22 | export class SummaryComponent extends CoreComponent {
23 |
24 | public serverSummary: string;
25 | public mysqlSummary: string;
26 | public mongoSummary: string;
27 |
28 | public serverSummaryError: string;
29 | public mysqlSummaryError: string;
30 | public mongoSummaryError: string;
31 |
32 | public serverSummaryLoader: boolean;
33 | public mysqlSummaryLoader: boolean;
34 | public mongoSummaryLoader: boolean;
35 |
36 | constructor(protected route: ActivatedRoute, protected router: Router,
37 | protected summaryService: SummaryService, protected instanceService: InstanceService) {
38 | super(route, router, instanceService);
39 | }
40 |
41 | /**
42 | * Gets MySQL summary via API, Agent from `pt-summary`.
43 | * @param agentUUID agent UUID that is installed on same host as MySQL.
44 | */
45 | getServerSummary(agentUUID: string): void {
46 | this.summaryService
47 | .getServer(agentUUID, this.dbServer.ParentUUID)
48 | .then(data => this.serverSummary = data)
49 | .catch(err => this.serverSummaryError = err.message)
50 | .then(() => this.serverSummaryLoader = false);
51 | }
52 |
53 | /**
54 | * Gets MySQL summary via API, Agent from `pt-mysql-summary`.
55 | * @param agentUUID agent UUID that is monitoring MySQL Server.
56 | */
57 | getMySQLSummary(agentUUID: string): void {
58 | this.summaryService
59 | .getMySQL(agentUUID, this.dbServer.UUID)
60 | .then(data => this.mysqlSummary = data)
61 | .catch(err => this.mysqlSummaryError = err.message)
62 | .then(() => this.mysqlSummaryLoader = false);
63 | }
64 |
65 | /**
66 | * Gets MongoDB summary via API, Agent from `pt-mongodb-summary`.
67 | * @param agentUUID agent UUID that is monitoring MongoDB Server.
68 | */
69 | getMongoSummary(agentUUID: string): void {
70 | this.summaryService
71 | .getMongo(agentUUID, this.dbServer.UUID)
72 | .then(data => this.mongoSummary = data)
73 | .catch(err => this.mongoSummaryError = err.message)
74 | .then(() => this.mongoSummaryLoader = false);
75 | }
76 |
77 | downloadSummary() {
78 | const momentFormatPipe = new MomentFormatPipe();
79 | const date = momentFormatPipe.transform(moment.utc(), 'YYYY-MM-DDTHH:mm:ss');
80 | const filename = `pmm-${this.dbServer.Name}-${date}-summary.zip`;
81 | const zip = new JSZip();
82 | zip.file('system_summary.txt', this.serverSummary);
83 | if (this.dbServer.Subsystem === 'mongo') {
84 | zip.file('server_summary.txt', this.mongoSummary);
85 | } else if (this.dbServer.Subsystem === 'mysql') {
86 | zip.file('server_summary.txt', this.mysqlSummary);
87 | }
88 | zip.generateAsync({type: 'blob'})
89 | .then(function(content) {
90 | // see FileSaver.js
91 | saveAs(content, filename);
92 | });
93 | }
94 |
95 |
96 | /**
97 | * Ovverrides parent method.
98 | * Executes on route was changed to refresh data.
99 | * @param params - URL query parameters
100 | */
101 | onChangeParams(params) {
102 | // to initalise loader when host was changed
103 | this.mysqlSummary = '',
104 | this.mongoSummary = '',
105 | this.serverSummary = '',
106 | this.mysqlSummaryError = '',
107 | this.mongoSummaryError = '',
108 | this.serverSummaryError = '';
109 | this.mysqlSummaryLoader = true,
110 | this.mongoSummaryLoader = true,
111 | this.serverSummaryLoader = true;
112 |
113 | this.getServerSummary(this.agent.UUID);
114 | if (this.dbServer.Subsystem === 'mysql') {
115 | this.getMySQLSummary(this.agent.UUID);
116 | }
117 | if (this.dbServer.Subsystem === 'mongo') {
118 | this.getMongoSummary(this.agent.UUID)
119 | }
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/src/app/summary/summary.service.spec.ts:
--------------------------------------------------------------------------------
1 | /* tslint:disable:no-unused-variable */
2 |
3 | import { TestBed, async, inject } from '@angular/core/testing';
4 | import { SummaryService } from './summary.service';
5 |
6 | describe('SummaryService', () => {
7 | beforeEach(() => {
8 | TestBed.configureTestingModule({
9 | providers: [SummaryService]
10 | });
11 | });
12 |
13 | it('should ...', inject([SummaryService], (service: SummaryService) => {
14 | expect(service).toBeTruthy();
15 | }));
16 | });
17 |
--------------------------------------------------------------------------------
/src/app/summary/summary.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import {HttpClient, HttpHeaders} from '@angular/common/http';
3 |
4 | @Injectable()
5 | export class SummaryService {
6 |
7 | private headers = new HttpHeaders({ 'Content-Type': 'application/json' });
8 |
9 | constructor(private httpClient: HttpClient) { }
10 |
11 | public getServer(agentUUID: string, serverUUID: string) {
12 | const url = `/qan-api/agents/${agentUUID}/cmd`;
13 | const data = {
14 | UUID: serverUUID,
15 | };
16 | const params = {
17 | AgentUUID: agentUUID,
18 | Service: 'query',
19 | Cmd: 'Summary',
20 | Data: btoa(JSON.stringify(data))
21 | };
22 |
23 | return this.httpClient
24 | .put(url, params, { headers: this.headers })
25 | .toPromise()
26 | .then(resp => {
27 | // if not error - continue
28 | if (!resp['Error']) {
29 | return resp;
30 | }
31 | let err = resp['Error'];
32 | if (resp['Error'] === 'Executable file not found in $PATH') {
33 | err = ' - Please install `pt-summary`.';
34 | err += ' (Output: ' + resp['Error'] + ')';
35 | }
36 | throw new Error(err);
37 | })
38 | .then(resp => {
39 | let str = window.atob(resp['Data']);
40 | str = str.replace(/\\n/g, '\n');
41 | str = str.replace(/\\t/g, '\t');
42 | return str.slice(1, -1);
43 | });
44 | }
45 |
46 | getMySQL(agentUUID: string, dbServerUUID: string) {
47 | const url = `/qan-api/agents/${agentUUID}/cmd`;
48 | const data = {
49 | UUID: dbServerUUID
50 | };
51 | const params = {
52 | AgentUUID: agentUUID,
53 | Service: 'query',
54 | Cmd: 'Summary',
55 | Data: btoa(JSON.stringify(data))
56 | };
57 |
58 | return this.httpClient
59 | .put(url, params, { headers: this.headers })
60 | .toPromise()
61 | .then(resp => {
62 | // if not error - continue
63 | if (!resp['Error']) {
64 | return resp;
65 | }
66 | let err = resp['Error'];
67 | if (resp['Error'] === 'Executable file not found in $PATH') {
68 | err = ' - Please install `pt-mysql-summary`.';
69 | err += ' (Output: ' + resp['Error'] + ')';
70 | }
71 | throw new Error(err);
72 | })
73 | .then(resp => {
74 | let str = window.atob(resp['Data']);
75 | str = str.replace(/\\n/g, '\n');
76 | str = str.replace(/\\t/g, '\t');
77 | return str.slice(1, -1);
78 | });
79 | }
80 |
81 | getMongo(agentUUID: string, dbServerUUID: string) {
82 | const url = `/qan-api/agents/${agentUUID}/cmd`;
83 | const data = {
84 | UUID: dbServerUUID
85 | };
86 |
87 | const params = {
88 | AgentUUID: agentUUID,
89 | Service: 'query',
90 | Cmd: 'Summary',
91 | Data: btoa(JSON.stringify(data))
92 | };
93 |
94 | return this.httpClient
95 | .put(url, params, { headers: this.headers })
96 | .toPromise()
97 | .then(resp => {
98 | // if not error - continue
99 | if (!resp['Error']) {
100 | return resp;
101 | }
102 | let err = resp['Error'];
103 | if (resp['Error'] === 'Executable file not found in $PATH') {
104 | err = ' - Please install `pt-mongodb-summary`.';
105 | err += ' (Output: ' + resp['Error'] + ')';
106 | }
107 | if (resp['Error'] === 'Unknown command: GetMongoSummary') {
108 | err = ' - Please update your `pmm-client`.';
109 | err += ' (Output: ' + resp['Error'] + ')';
110 | }
111 | throw new Error(err);
112 | })
113 | .then(resp => {
114 | let str = window.atob(resp['Data']);
115 | str = str.replace(/\\n/g, '\n');
116 | str = str.replace(/\\t/g, '\t');
117 | return str.slice(1, -1);
118 | });
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/percona/qan-app/dc15e33d41a0b87abec08abb83aded5b0f83356a/src/assets/.gitkeep
--------------------------------------------------------------------------------
/src/assets/50747258.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/percona/qan-app/dc15e33d41a0b87abec08abb83aded5b0f83356a/src/assets/50747258.png
--------------------------------------------------------------------------------
/src/assets/Percona_XtraDB_Cluster.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/percona/qan-app/dc15e33d41a0b87abec08abb83aded5b0f83356a/src/assets/Percona_XtraDB_Cluster.png
--------------------------------------------------------------------------------
/src/assets/background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/percona/qan-app/dc15e33d41a0b87abec08abb83aded5b0f83356a/src/assets/background.png
--------------------------------------------------------------------------------
/src/assets/database.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/percona/qan-app/dc15e33d41a0b87abec08abb83aded5b0f83356a/src/assets/database.png
--------------------------------------------------------------------------------
/src/assets/footer-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/percona/qan-app/dc15e33d41a0b87abec08abb83aded5b0f83356a/src/assets/footer-logo.png
--------------------------------------------------------------------------------
/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/percona/qan-app/dc15e33d41a0b87abec08abb83aded5b0f83356a/src/assets/logo.png
--------------------------------------------------------------------------------
/src/assets/mysql_64_black.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/percona/qan-app/dc15e33d41a0b87abec08abb83aded5b0f83356a/src/assets/mysql_64_black.png
--------------------------------------------------------------------------------
/src/assets/percona-server-black-50.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/percona/qan-app/dc15e33d41a0b87abec08abb83aded5b0f83356a/src/assets/percona-server-black-50.png
--------------------------------------------------------------------------------
/src/assets/pmm-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
24 |
25 |
60 |
68 |
69 |
70 |
72 |
73 |
74 |
75 |
76 |
79 |
80 |
--------------------------------------------------------------------------------
/src/assets/sm-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/percona/qan-app/dc15e33d41a0b87abec08abb83aded5b0f83356a/src/assets/sm-logo.png
--------------------------------------------------------------------------------
/src/environments/environment.prod.ts:
--------------------------------------------------------------------------------
1 | export const environment = {
2 | production: true,
3 | demoHosts: ['163.172.51.248', 'pmmdemo.percona.com'],
4 | version: require('../../package.json').version
5 | };
6 |
--------------------------------------------------------------------------------
/src/environments/environment.ts:
--------------------------------------------------------------------------------
1 | // The file contents for the current environment will overwrite these during build.
2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do
3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead.
4 | // The list of which env maps to which file can be found in `angular-cli.json`.
5 |
6 | export const environment = {
7 | production: false,
8 | demoHosts: ['163.172.51.248', 'pmmdemo.percona.com'],
9 | version: 'dev'
10 | };
11 |
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/percona/qan-app/dc15e33d41a0b87abec08abb83aded5b0f83356a/src/favicon.ico
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Percona Query Analytics
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
2 | import { enableProdMode } from '@angular/core';
3 | import { environment } from './environments/environment';
4 | import { AppModule } from './app/app.module';
5 |
6 | if (environment.production) {
7 | enableProdMode();
8 | }
9 |
10 | platformBrowserDynamic().bootstrapModule(AppModule);
11 |
--------------------------------------------------------------------------------
/src/polyfills.ts:
--------------------------------------------------------------------------------
1 | // This file includes polyfills needed by Angular and is loaded before
2 | // the app. You can add your own extra polyfills to this file.
3 | import 'core-js/es6/symbol';
4 | import 'core-js/es6/object';
5 | import 'core-js/es6/function';
6 | import 'core-js/es6/parse-int';
7 | import 'core-js/es6/parse-float';
8 | import 'core-js/es6/number';
9 | import 'core-js/es6/math';
10 | import 'core-js/es6/string';
11 | import 'core-js/es6/date';
12 | import 'core-js/es6/array';
13 | import 'core-js/es6/regexp';
14 | import 'core-js/es6/map';
15 | import 'core-js/es6/set';
16 | import 'core-js/es6/reflect';
17 |
18 | import 'core-js/es7/reflect';
19 | import 'zone.js/dist/zone';
20 |
--------------------------------------------------------------------------------
/src/test.ts:
--------------------------------------------------------------------------------
1 | import 'zone.js/dist/long-stack-trace-zone';
2 | import 'zone.js/dist/proxy.js';
3 | import 'zone.js/dist/sync-test';
4 | import 'zone.js/dist/jasmine-patch';
5 | import 'zone.js/dist/async-test';
6 | import 'zone.js/dist/fake-async-test';
7 | import { getTestBed } from '@angular/core/testing';
8 | import {
9 | BrowserDynamicTestingModule,
10 | platformBrowserDynamicTesting
11 | } from '@angular/platform-browser-dynamic/testing';
12 |
13 | // Unfortunately there's no typing for the `__karma__` variable. Just declare it as any.
14 | declare var __karma__: any;
15 | declare var require: any;
16 |
17 | // Prevent Karma from running prematurely.
18 | __karma__.loaded = function () {};
19 |
20 | // First, initialize the Angular testing environment.
21 | getTestBed().initTestEnvironment(
22 | BrowserDynamicTestingModule,
23 | platformBrowserDynamicTesting()
24 | );
25 | // Then we find all the tests.
26 | const context = require.context('./', true, /\.spec\.ts$/);
27 | // And load the modules.
28 | context.keys().map(context);
29 | // Finally, start Karma to run the tests.
30 | __karma__.start();
31 |
--------------------------------------------------------------------------------
/src/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "sourceMap": true,
4 | "declaration": false,
5 | "moduleResolution": "node",
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "lib": [
9 | "es2016",
10 | "dom"
11 | ],
12 | "outDir": "../out-tsc/app",
13 | "target": "es5",
14 | "module": "es2015",
15 | "baseUrl": "",
16 | "types": []
17 | },
18 | "exclude": [
19 | "test.ts",
20 | "**/*.spec.ts"
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/src/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "sourceMap": true,
4 | "declaration": false,
5 | "moduleResolution": "node",
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "lib": [
9 | "es2016",
10 | "dom"
11 | ],
12 | "outDir": "../out-tsc/spec",
13 | "module": "commonjs",
14 | "target": "es6",
15 | "baseUrl": "",
16 | "types": [
17 | "jasmine",
18 | "node"
19 | ]
20 | },
21 | "files": [
22 | "test.ts",
23 | "polyfills.ts"
24 | ],
25 | "include": [
26 | "**/*.spec.ts"
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------
/src/typings.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'moment-timezone';
2 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compileOnSave": false,
3 | "compilerOptions": {
4 | "outDir": "./dist/out-tsc",
5 | "sourceMap": true,
6 | "declaration": false,
7 | "moduleResolution": "node",
8 | "emitDecoratorMetadata": true,
9 | "experimentalDecorators": true,
10 | "lib": [
11 | "es2016",
12 | "dom"
13 | ],
14 | "module": "es2015",
15 | "baseUrl": "./"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/tslint.json:
--------------------------------------------------------------------------------
1 | {
2 | "rulesDirectory": [
3 | "node_modules/codelyzer"
4 | ],
5 | "rules": {
6 | "callable-types": true,
7 | "class-name": true,
8 | "comment-format": [
9 | true,
10 | "check-space"
11 | ],
12 | "curly": true,
13 | "eofline": true,
14 | "forin": true,
15 | "import-blacklist": [true],
16 | "import-spacing": true,
17 | "indent": [
18 | true,
19 | "spaces"
20 | ],
21 | "interface-over-type-literal": true,
22 | "label-position": true,
23 | "max-line-length": [
24 | true,
25 | 140
26 | ],
27 | "member-access": false,
28 | "member-ordering": [
29 | true,
30 | "static-before-instance",
31 | "variables-before-functions"
32 | ],
33 | "no-arg": true,
34 | "no-bitwise": true,
35 | "no-console": [
36 | true,
37 | "debug",
38 | "info",
39 | "time",
40 | "timeEnd",
41 | "trace"
42 | ],
43 | "no-construct": true,
44 | "no-debugger": true,
45 | "no-duplicate-variable": true,
46 | "no-empty": false,
47 | "no-empty-interface": true,
48 | "no-eval": true,
49 | "no-inferrable-types": [true, "ignore-params"],
50 | "no-shadowed-variable": true,
51 | "no-string-literal": false,
52 | "no-string-throw": true,
53 | "no-switch-case-fall-through": true,
54 | "no-trailing-whitespace": true,
55 | "no-unused-expression": true,
56 | "no-use-before-declare": true,
57 | "no-var-keyword": true,
58 | "object-literal-sort-keys": false,
59 | "one-line": [
60 | true,
61 | "check-open-brace",
62 | "check-catch",
63 | "check-else",
64 | "check-whitespace"
65 | ],
66 | "prefer-const": true,
67 | "quotemark": [
68 | true,
69 | "single"
70 | ],
71 | "radix": true,
72 | "semicolon": [
73 | "always"
74 | ],
75 | "triple-equals": [
76 | true,
77 | "allow-null-check"
78 | ],
79 | "typedef-whitespace": [
80 | true,
81 | {
82 | "call-signature": "nospace",
83 | "index-signature": "nospace",
84 | "parameter": "nospace",
85 | "property-declaration": "nospace",
86 | "variable-declaration": "nospace"
87 | }
88 | ],
89 | "unified-signatures": true,
90 | "variable-name": false,
91 | "whitespace": [
92 | true,
93 | "check-branch",
94 | "check-decl",
95 | "check-operator",
96 | "check-separator",
97 | "check-type"
98 | ],
99 |
100 | "directive-selector": [true, "attribute", "app", "camelCase"],
101 | "component-selector": [true, "element", "app", "kebab-case"],
102 | "no-inputs-metadata-property": true,
103 | "no-outputs-metadata-property": true,
104 | "no-host-metadata-property": true,
105 | "no-input-rename": true,
106 | "no-output-rename": true,
107 | "use-life-cycle-interface": true,
108 | "use-pipe-transform-interface": true,
109 | "component-class-suffix": true,
110 | "directive-class-suffix": true
111 | }
112 | }
113 |
--------------------------------------------------------------------------------