├── .editorconfig ├── .gitignore ├── .jshintrc ├── Dockerfile ├── LICENSE ├── README.md ├── app ├── assets │ ├── css │ │ └── styles.css │ ├── fonts │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ ├── fontawesome-webfont.woff2 │ │ ├── noto-sans-700.ttf │ │ ├── noto-sans-700italic.ttf │ │ ├── noto-sans-italic.ttf │ │ ├── noto-sans-regular.ttf │ │ ├── source-code-pro-300.ttf │ │ └── source-code-pro-500.ttf │ ├── img │ │ └── pev-logo.png │ ├── sass │ │ ├── _buttons.scss │ │ ├── _common.scss │ │ ├── _fonts.scss │ │ ├── _footer.scss │ │ ├── _forms.scss │ │ ├── _highlight.scss │ │ ├── _menu.scss │ │ ├── _modal.scss │ │ ├── _nav.scss │ │ ├── _page.scss │ │ ├── _plan-node.scss │ │ ├── _plan.scss │ │ ├── _table.scss │ │ ├── _variables.scss │ │ ├── font-awesome │ │ │ ├── _animated.scss │ │ │ ├── _bordered-pulled.scss │ │ │ ├── _core.scss │ │ │ ├── _fixed-width.scss │ │ │ ├── _icons.scss │ │ │ ├── _larger.scss │ │ │ ├── _list.scss │ │ │ ├── _mixins.scss │ │ │ ├── _path.scss │ │ │ ├── _rotated-flipped.scss │ │ │ ├── _stacked.scss │ │ │ ├── _styles.scss │ │ │ └── _variables.scss │ │ └── styles.scss │ └── styles.css ├── bootstrap.ts ├── components │ ├── about │ │ ├── about.html │ │ └── about.ts │ ├── app │ │ ├── app.html │ │ └── app.ts │ ├── plan-list │ │ ├── plan-list.html │ │ └── plan-list.ts │ ├── plan-new │ │ ├── plan-new.html │ │ └── plan-new.ts │ ├── plan-node │ │ ├── plan-node.html │ │ └── plan-node.ts │ └── plan-view │ │ ├── plan-view.html │ │ └── plan-view.ts ├── enums.ts ├── index.html ├── interfaces │ └── iplan.ts ├── pipes.ts ├── sample-plans │ └── plan1.json └── services │ ├── color-service.ts │ ├── help-service.ts │ ├── plan-service.ts │ └── syntax-highlight-service.ts ├── docker-compose.yml ├── gulpfile.ts ├── karma.conf.js ├── logo.ai ├── package.json ├── test-main.js ├── tools ├── config.ts ├── tasks │ ├── build.bundles.ts │ ├── build.deps.ts │ ├── build.docs.ts │ ├── build.fonts.dev.ts │ ├── build.html_css.prod.ts │ ├── build.img.dev.ts │ ├── build.index.ts │ ├── build.js.dev.ts │ ├── build.js.prod.ts │ ├── build.sass.dev.ts │ ├── build.test.ts │ ├── check.versions.ts │ ├── clean.ts │ ├── karma.start.ts │ ├── npm.ts │ ├── serve.docs.ts │ ├── server.start.ts │ ├── tsd.ts │ ├── tslint.ts │ ├── watch.dev.ts │ ├── watch.serve.ts │ └── watch.test.ts ├── typings │ ├── connect-livereload.d.ts │ ├── gulp-load-plugins.d.ts │ ├── karma.d.ts │ ├── merge-stream.d.ts │ ├── open.d.ts │ ├── run-sequence.d.ts │ ├── slash.d.ts │ ├── systemjs-builder.d.ts │ ├── tiny-lr.d.ts │ └── yargs.d.ts ├── utils.ts └── utils │ ├── server.ts │ ├── tasks_tools.ts │ ├── template-injectables.ts │ └── template-locals.ts ├── tsconfig.json ├── tsd.json ├── tsdrc └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | insert_final_newline = true 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | insert_final_newline = false 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | node_modules 23 | .sass-cache 24 | 25 | # Users Environment Variables 26 | .lock-wscript 27 | .tsdrc 28 | 29 | #IDE configuration files 30 | .idea 31 | .vscode 32 | 33 | dist 34 | dev 35 | docs 36 | lib 37 | test 38 | tools/typings/tsd 39 | tmp 40 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "bitwise": true, 3 | "immed": true, 4 | "newcap": true, 5 | "noarg": true, 6 | "noempty": true, 7 | "nonew": true, 8 | "trailing": true, 9 | "maxlen": 200, 10 | "boss": true, 11 | "eqnull": true, 12 | "expr": true, 13 | "globalstrict": true, 14 | "laxbreak": true, 15 | "loopfunc": true, 16 | "sub": true, 17 | "undef": true, 18 | "indent": 2, 19 | "unused": true, 20 | 21 | "node": true, 22 | "globals": { 23 | "System": true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:jessie 2 | 3 | RUN apt-get update && apt-get -y install apt-transport-https curl g++ make python ruby 4 | 5 | RUN curl -sL https://deb.nodesource.com/setup_4.x | bash - 6 | 7 | RUN apt-get -y install nodejs 8 | 9 | COPY tsdrc ~/.tsdrc 10 | 11 | COPY . /var/www/pev 12 | 13 | WORKDIR /var/www/pev 14 | 15 | RUN npm install -g gulp 16 | 17 | RUN npm install 18 | 19 | CMD cd /var/www/pev && npm start 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Alex Tatiyants 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Postgres Explain Visualizer (pev) 2 | 3 | Postgres Explain Visualizer (dev) is designed to make [EXPLAIN](http://www.postgresql.org/docs/current/static/sql-explain.html) output easier to grok. It creates a graphical representation of the plan. You can see it in action at [tatiyants.com/pev](http://tatiyants.com/pev/), or read about it [on my blog](http://tatiyants.com/postgres-query-plan-visualization/). 4 | 5 | Pev is heavily influenced by the excellent [explain.depesz.com](http://explain.depesz.com/). 6 | 7 | Pev is written in [angular 2](https://angular.io/) with [TypeScript](http://www.typescriptlang.org/). The project is based on [angular2 seed](https://github.com/mgechev/angular2-seed). It requires [npm](https://www.npmjs.com/), [gulp](http://gulpjs.com/), [tsd](http://definitelytyped.org/tsd/), and [compass](http://compass-style.org/). 8 | 9 | 10 | ## Installation 11 | 12 | ``` 13 | npm install 14 | npm start 15 | ``` 16 | 17 | You may also need to install tsd and compass: 18 | 19 | ``` 20 | npm install tsd -g 21 | gem install compass 22 | ``` 23 | 24 | ## Build 25 | To build, run the build command for a specific environment. For example, the following will create a production distribution: 26 | 27 | ``` 28 | npm start build.prod 29 | ``` 30 | -------------------------------------------------------------------------------- /app/assets/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexTatiyants/pev/6d31cdd75f557761d7581da6c46586792e5f2dad/app/assets/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /app/assets/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexTatiyants/pev/6d31cdd75f557761d7581da6c46586792e5f2dad/app/assets/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /app/assets/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexTatiyants/pev/6d31cdd75f557761d7581da6c46586792e5f2dad/app/assets/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /app/assets/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexTatiyants/pev/6d31cdd75f557761d7581da6c46586792e5f2dad/app/assets/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /app/assets/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexTatiyants/pev/6d31cdd75f557761d7581da6c46586792e5f2dad/app/assets/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /app/assets/fonts/noto-sans-700.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexTatiyants/pev/6d31cdd75f557761d7581da6c46586792e5f2dad/app/assets/fonts/noto-sans-700.ttf -------------------------------------------------------------------------------- /app/assets/fonts/noto-sans-700italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexTatiyants/pev/6d31cdd75f557761d7581da6c46586792e5f2dad/app/assets/fonts/noto-sans-700italic.ttf -------------------------------------------------------------------------------- /app/assets/fonts/noto-sans-italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexTatiyants/pev/6d31cdd75f557761d7581da6c46586792e5f2dad/app/assets/fonts/noto-sans-italic.ttf -------------------------------------------------------------------------------- /app/assets/fonts/noto-sans-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexTatiyants/pev/6d31cdd75f557761d7581da6c46586792e5f2dad/app/assets/fonts/noto-sans-regular.ttf -------------------------------------------------------------------------------- /app/assets/fonts/source-code-pro-300.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexTatiyants/pev/6d31cdd75f557761d7581da6c46586792e5f2dad/app/assets/fonts/source-code-pro-300.ttf -------------------------------------------------------------------------------- /app/assets/fonts/source-code-pro-500.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexTatiyants/pev/6d31cdd75f557761d7581da6c46586792e5f2dad/app/assets/fonts/source-code-pro-500.ttf -------------------------------------------------------------------------------- /app/assets/img/pev-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexTatiyants/pev/6d31cdd75f557761d7581da6c46586792e5f2dad/app/assets/img/pev-logo.png -------------------------------------------------------------------------------- /app/assets/sass/_buttons.scss: -------------------------------------------------------------------------------- 1 | .btn { 2 | border-radius: $border-radius-base; 3 | padding: $padding-base $padding-lg; 4 | font-size: $font-size-base; 5 | line-height: 1.2; 6 | text-decoration: none; 7 | text-transform: uppercase; 8 | cursor: pointer; 9 | margin-left: $padding-base; 10 | 11 | &-default { 12 | border: 1px solid $blue; 13 | color: $blue; 14 | background-color: #fff; 15 | 16 | &:hover { 17 | background-color: $light-blue; 18 | color: #fff; 19 | } 20 | } 21 | 22 | &-lg { 23 | padding: $padding-base $padding-lg*2; 24 | font-size: round($font-size-base * 1.2); 25 | } 26 | 27 | &-sm { 28 | padding: $padding-base $padding-base; 29 | font-size: round($font-size-base * 0.9); 30 | } 31 | 32 | &-slim { 33 | .fa { margin: 0}; 34 | } 35 | 36 | &-link { 37 | border: 0; 38 | background-color: transparent; 39 | color: $link-color; 40 | padding: 0; 41 | } 42 | 43 | &-danger { 44 | border: 1px solid $red; 45 | color: $red; 46 | text-transform: uppercase; 47 | background-color: #fff; 48 | 49 | &:hover { 50 | background-color: $light-red; 51 | } 52 | } 53 | 54 | &-primary { 55 | border: 0; 56 | background: $blue; 57 | box-shadow: 1px 1px 1px $gray; 58 | color: #ffffff; 59 | 60 | &:hover { 61 | background: $dark-blue; 62 | } 63 | } 64 | 65 | &-close { 66 | @extend .btn-slim; 67 | border-radius: 50%; 68 | box-shadow: 0; 69 | line-height: 1.5; 70 | border: 1px solid $line-color; 71 | background-color: #fff; 72 | } 73 | } 74 | 75 | .button-group { 76 | button { 77 | margin: 0; 78 | background-color: $gray-lightest; 79 | border-radius: 0; 80 | float: left; 81 | border: 1px solid $line-color; 82 | cursor: pointer; 83 | 84 | &:first-of-type { 85 | border-top-left-radius: $border-radius-lg; 86 | border-bottom-left-radius: $border-radius-lg; 87 | } 88 | &:last-of-type { 89 | border-top-right-radius: $border-radius-lg; 90 | border-bottom-right-radius: $border-radius-lg; 91 | } 92 | } 93 | 94 | .selected { 95 | background-color: $gray-light; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /app/assets/sass/_common.scss: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | font-size: $font-size-base; 7 | font-weight: 300; 8 | color: $text-color; 9 | height: 100%; 10 | width: 100%; 11 | background-color: $bg-color; 12 | line-height: $line-height-base; 13 | } 14 | 15 | strong { 16 | font-weight: 600; 17 | } 18 | 19 | body, input, a, button, textarea { 20 | font-family: $font-family-sans-serif; 21 | font-weight: 300; 22 | } 23 | 24 | .text-muted { 25 | color: $text-color-light; 26 | } 27 | 28 | .text-warning { 29 | color: $alert-color; 30 | } 31 | 32 | .hero-container { 33 | margin: $padding-lg * 3; 34 | font-size: $font-size-xl; 35 | text-align: center; 36 | } 37 | 38 | .pull-right { 39 | float: right; 40 | } 41 | 42 | .align-right { 43 | text-align: right; 44 | } 45 | 46 | a { 47 | color: $link-color; 48 | text-decoration: none; 49 | } 50 | 51 | .fa { 52 | margin-right: $padding-sm; 53 | } 54 | 55 | .clickable { 56 | cursor: pointer; 57 | } 58 | 59 | code, .code { 60 | font-family: $font-family-mono; 61 | font-weight: 600; 62 | } 63 | 64 | .pad-left { 65 | margin-left: $padding-lg; 66 | } 67 | 68 | .pad-top { 69 | margin-top: $padding-lg; 70 | } 71 | 72 | .pad-bottom { 73 | margin-bottom: $padding-lg; 74 | } 75 | 76 | [tooltip]:before { 77 | width: 150px; 78 | text-transform: none; 79 | text-align: left; 80 | content: attr(tooltip); 81 | position: absolute; 82 | opacity: 0; 83 | transition: all 0.15s ease; 84 | padding: $padding-lg; 85 | color: #fff; 86 | border-radius: $border-radius-lg; 87 | margin-top: -10px; 88 | margin-left: 20px; 89 | z-index: 5; 90 | pointer-events: none; 91 | } 92 | 93 | [tooltip]:hover:before { 94 | opacity: 1; 95 | background: $dark-blue; 96 | } 97 | -------------------------------------------------------------------------------- /app/assets/sass/_fonts.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'noto'; 3 | src: url('../fonts/noto-sans-regular.ttf') format('truetype'); 4 | font-weight: 300; 5 | font-style: normal; 6 | } 7 | 8 | @font-face { 9 | font-family: 'noto'; 10 | src: url('../fonts/noto-sans-italic.ttf') format('truetype'); 11 | font-weight: 300; 12 | font-style: italic; 13 | } 14 | 15 | @font-face { 16 | font-family: 'noto'; 17 | src: url('../fonts/noto-sans-700.ttf') format('truetype'); 18 | font-weight: 600; 19 | font-style: normal; 20 | } 21 | 22 | @font-face { 23 | font-family: 'noto'; 24 | src: url('../fonts/noto-sans-700italic.ttf') format('truetype'); 25 | font-weight: 600; 26 | font-style: italic; 27 | } 28 | 29 | @font-face { 30 | font-family: 'source code'; 31 | src: url('../fonts/source-code-pro-300.ttf') format('truetype'); 32 | font-weight: 300; 33 | font-style: normal; 34 | } 35 | 36 | @font-face { 37 | font-family: 'source code'; 38 | src: url('../fonts/source-code-pro-500.ttf') format('truetype'); 39 | font-weight: 600; 40 | font-style: normal; 41 | } 42 | -------------------------------------------------------------------------------- /app/assets/sass/_footer.scss: -------------------------------------------------------------------------------- 1 | footer { 2 | border-top: 2px solid $gray-light; 3 | margin: round($padding-lg * 2); 4 | padding: round($padding-lg * 2); 5 | color: $gray; 6 | text-align: center; 7 | width: 600px; 8 | margin: auto; 9 | 10 | .fa { 11 | font-size: $font-size-lg; 12 | margin-left: $padding-base; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /app/assets/sass/_forms.scss: -------------------------------------------------------------------------------- 1 | @import "compass/css3/user-interface"; 2 | 3 | .input-box { 4 | @include input-placeholder { 5 | color: $gray; 6 | font-size: round($font-size-base * 1.4); 7 | } 8 | 9 | &:focus { 10 | box-shadow: 0 0 5px rgba(81, 203, 238, 1); 11 | } 12 | 13 | &-main { 14 | font-size: round($font-size-base * 1.4); 15 | width: 700px; 16 | border: 0; 17 | border-bottom: 2px solid $blue; 18 | padding: $padding-lg; 19 | margin-top: $padding-lg; 20 | margin-bottom: $padding-lg; 21 | } 22 | 23 | &-lg { 24 | width: 98%; 25 | height: 210px; 26 | margin-bottom: $padding-base; 27 | margin-bottom: $padding-lg; 28 | border-radius: $border-radius-base; 29 | border: 1px solid $line-color; 30 | padding: $padding-lg; 31 | } 32 | } 33 | 34 | .error-message { 35 | background-color: $alert-color; 36 | padding: $padding-base; 37 | color: #fff; 38 | } 39 | -------------------------------------------------------------------------------- /app/assets/sass/_highlight.scss: -------------------------------------------------------------------------------- 1 | .plan-query-container { 2 | border: 1px solid $line-color; 3 | padding: $padding-xl; 4 | background-color: #fff; 5 | position: absolute; 6 | box-shadow: 0px 0px 10px 2px rgba(0,0,0,0.3); 7 | border-radius: $border-radius-base; 8 | margin-bottom: $padding-xl; 9 | z-index: 6; 10 | left: 0; 11 | 12 | code { 13 | font-weight: 300; 14 | } 15 | 16 | .plan-query-text { 17 | background-color: #fff; 18 | font-family: $font-family-mono; 19 | text-align: left; 20 | 21 | .code-key-item { 22 | background-color: $yellow; 23 | font-weight: 600; 24 | padding: 1px; 25 | } 26 | } 27 | 28 | h3 { 29 | font-size: $font-size-lg; 30 | width: 93%; 31 | text-align: left; 32 | border-bottom: 1px solid $line-color; 33 | padding-bottom: $padding-base; 34 | margin-bottom: $padding-lg; 35 | } 36 | } 37 | 38 | .hljs, .hljs-subst { 39 | color: $text-color; 40 | } 41 | 42 | .hljs-keyword, 43 | .hljs-attribute, 44 | .hljs-selector-tag, 45 | .hljs-meta-keyword, 46 | .hljs-doctag, 47 | .hljs-name { 48 | color: $dark-blue; 49 | font-weight: bold; 50 | } 51 | 52 | .hljs-built_in, 53 | .hljs-literal, 54 | .hljs-bullet, 55 | .hljs-code, 56 | .hljs-addition { 57 | color: $dark-blue; 58 | font-weight: bold; 59 | } 60 | 61 | .hljs-regexp, 62 | .hljs-symbol, 63 | .hljs-variable, 64 | .hljs-template-variable, 65 | .hljs-link, 66 | .hljs-selector-attr, 67 | .hljs-selector-pseudo { 68 | color: $dark-blue; 69 | font-weight: bold; 70 | } 71 | 72 | .hljs-type, 73 | .hljs-string, 74 | .hljs-number, 75 | .hljs-selector-id, 76 | .hljs-selector-class, 77 | .hljs-quote, 78 | .hljs-template-tag, 79 | .hljs-deletion { 80 | color: $green; 81 | font-weight: 600; 82 | } 83 | 84 | .hljs-title, 85 | .hljs-section { 86 | color: $dark-blue; 87 | font-weight: bold; 88 | } 89 | 90 | .hljs-comment { 91 | color: $text-color-light; 92 | font-style: italic; 93 | } 94 | 95 | .hljs-meta { 96 | color: $text-color-light; 97 | } 98 | 99 | .hljs-emphasis { 100 | font-style: italic; 101 | } 102 | 103 | .hljs-strong { 104 | font-weight: 600; 105 | } 106 | -------------------------------------------------------------------------------- /app/assets/sass/_menu.scss: -------------------------------------------------------------------------------- 1 | $menu-offset: 120px; 2 | $menu-toggle-height: 45px; 3 | 4 | .menu { 5 | width: 190px; 6 | height: 260px; 7 | position: absolute; 8 | font-size: $font-size-sm; 9 | top: $menu-offset - 5; 10 | left: 0; 11 | background-color: $gray-dark; 12 | box-shadow: 1px 1px 2px 1px rgba(0,0,0,0.2); 13 | color: #fff; 14 | border-top-right-radius: $border-radius-base; 15 | border-bottom-right-radius: $border-radius-base; 16 | z-index: 1; 17 | 18 | header { 19 | h3 { 20 | padding-top: $padding-lg; 21 | margin-bottom: $padding-base; 22 | font-size: round($font-size-base * 1.2); 23 | font-weight: 600; 24 | line-height: 2; 25 | text-align: right; 26 | padding-right: $padding-lg * 2; 27 | } 28 | } 29 | 30 | ul { 31 | margin-left: $padding-lg; 32 | 33 | li { 34 | line-height: 2.5; 35 | } 36 | } 37 | 38 | &-toggle { 39 | font-size: round($font-size-lg * 1.3); 40 | float: left; 41 | padding-left: $padding-base; 42 | line-height: 2; 43 | cursor: pointer; 44 | } 45 | 46 | &-hidden { 47 | width: $menu-toggle-height; 48 | height: $menu-toggle-height; 49 | border-top-right-radius: 50%; 50 | border-bottom-right-radius: 50%; 51 | 52 | ul, h3 { 53 | visibility: hidden; 54 | } 55 | } 56 | 57 | .button-group { 58 | display: flex; 59 | margin-bottom: $padding-base; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /app/assets/sass/_modal.scss: -------------------------------------------------------------------------------- 1 | .modal-backdrop { 2 | position: fixed; 3 | top: 0; 4 | right: 0; 5 | bottom: 0; 6 | left: 0; 7 | z-index: 1040; 8 | background-color: #000; 9 | opacity: 0.1; 10 | } 11 | 12 | .modal { 13 | position: fixed; 14 | top: 0; 15 | right: 0; 16 | bottom: 0; 17 | left: 0; 18 | z-index: 1050; 19 | transition: all 1s; 20 | 21 | .modal-dialog { 22 | position: relative; 23 | transform: translate(0); 24 | margin: 100px auto; 25 | width: 500px; 26 | opacity: 1; 27 | 28 | .modal-content { 29 | padding: 30px; 30 | position: relative; 31 | background-color: #fff; 32 | background-clip: padding-box; 33 | border: 1px solid #999; 34 | border: 1px solid rgba(0,0,0,.2); 35 | border-radius: 6px; 36 | outline: 0; 37 | box-shadow: 0 2px 2px rgba(0,0,0,.5); 38 | display: block; 39 | 40 | .modal-body { 41 | padding: $padding-sm; 42 | margin-bottom: $padding-xl; 43 | text-align: left; 44 | line-height: 1.5; 45 | } 46 | 47 | .modal-footer { 48 | text-align: right; 49 | 50 | button { 51 | margin-left: $padding-sm; 52 | } 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /app/assets/sass/_nav.scss: -------------------------------------------------------------------------------- 1 | nav { 2 | font-size: round($font-size-base * 1.3); 3 | background-color: #fff; 4 | padding: round($padding-lg * 1.5); 5 | 6 | .nav-container { 7 | width: $page-width; 8 | margin: auto; 9 | } 10 | 11 | .about { 12 | float: right; 13 | line-height: 2; 14 | margin-left: $padding-xl; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/assets/sass/_page.scss: -------------------------------------------------------------------------------- 1 | .page { 2 | padding-top: $padding-lg; 3 | margin: auto; 4 | width: $page-width; 5 | min-height: 600px; 6 | 7 | em { 8 | font-style: italic; 9 | } 10 | } 11 | 12 | .page-content { 13 | h2 { 14 | font-size: round($font-size-base * 2); 15 | line-height: 2; 16 | } 17 | h3 { 18 | font-size: round($font-size-base * 1.7); 19 | margin-top: $padding-xl; 20 | line-height: 2; 21 | } 22 | 23 | p, ul { 24 | font-size: round($font-size-base * 1.2); 25 | line-height: 1.5; 26 | margin-bottom: $padding-xl; 27 | } 28 | 29 | ul { 30 | list-style-type: disc; 31 | position: relative; 32 | left: $padding-xl; 33 | } 34 | } 35 | 36 | .page-stretch { 37 | @extend .page; 38 | margin: auto; 39 | width: 95%; 40 | 41 | h2 { 42 | text-align: left; 43 | font-size: $font-size-lg; 44 | max-width: $page-width; 45 | margin: auto; 46 | margin-bottom: $padding-lg*2; 47 | 48 | .sub-title { 49 | font-size: $font-size-base; 50 | font-style: italic; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /app/assets/sass/_plan-node.scss: -------------------------------------------------------------------------------- 1 | .plan-node { 2 | text-decoration: none; 3 | color: $text-color; 4 | display: inline-block; 5 | transition: all 0.1s; 6 | position: relative; 7 | padding: $padding-base $padding-lg; 8 | background-color: #fff; 9 | font-size: $font-size-sm; 10 | border: 1px solid $line-color; 11 | margin-bottom: 4px; 12 | border-radius: $border-radius-base; 13 | overflow-wrap: break-word; 14 | word-wrap: break-word; 15 | word-break: break-all; 16 | width: 240px; 17 | box-shadow: 1px 1px 3px 0px rgba(0,0,0,0.1); 18 | 19 | header { 20 | margin-bottom: $padding-base; 21 | overflow: hidden; 22 | cursor: pointer; 23 | 24 | &:hover { 25 | background-color: $gray-lightest; 26 | } 27 | 28 | h4 { 29 | font-size: $font-size-base; 30 | float: left; 31 | font-weight: 600; 32 | } 33 | 34 | .node-duration { 35 | float: right; 36 | margin-left: $padding-lg; 37 | font-size: $font-size-base; 38 | } 39 | } 40 | 41 | .prop-list { 42 | float: left; 43 | text-align: left; 44 | overflow-wrap: break-word; 45 | word-wrap: break-word; 46 | word-break: break-all; 47 | margin-top: $padding-lg; 48 | margin-bottom: $padding-base; 49 | } 50 | 51 | .relation-name { 52 | text-align: left; 53 | } 54 | 55 | .planner-estimate { 56 | border-top: 1px solid $line-color; 57 | text-align: left; 58 | padding-top: $padding-sm; 59 | margin-top: $padding-base; 60 | width: 100%; 61 | } 62 | 63 | .tags { 64 | margin-top: $padding-base; 65 | text-align: left; 66 | 67 | span { 68 | display: inline-block; 69 | background-color: $alert-color; 70 | color: #fff; 71 | font-size: round($font-size-sm * 0.8); 72 | font-weight: 600; 73 | margin-right: $padding-sm; 74 | margin-bottom: $padding-sm; 75 | padding: $padding-sm; 76 | border-radius: $border-radius-base; 77 | line-height: 1.1; 78 | } 79 | } 80 | 81 | //hovers 82 | &:hover { 83 | border-color: $highlight-color; 84 | } 85 | 86 | .node-description { 87 | text-align: left; 88 | font-style: italic; 89 | padding-top: $padding-lg; 90 | word-break: normal; 91 | 92 | .node-type { 93 | font-weight: 600; 94 | background-color: $blue; 95 | color: #fff; 96 | padding: 0 $padding-base; 97 | } 98 | } 99 | 100 | .btn-default { 101 | border: 0; 102 | } 103 | } 104 | 105 | .node-bar-container { 106 | height: 5px; 107 | margin-top: $padding-lg; 108 | margin-bottom: $padding-sm; 109 | border-radius: $border-radius-lg; 110 | background-color: $gray-light; 111 | position: relative; 112 | 113 | .node-bar { 114 | border-radius: $border-radius-lg; 115 | height: 100%; 116 | text-align: left; 117 | position: absolute; 118 | left: 0; 119 | top: 0; 120 | } 121 | } 122 | 123 | .node-bar-label { 124 | text-align: left; 125 | display: block; 126 | } 127 | 128 | .expanded { 129 | width: 400px !important; 130 | overflow: visible !important; 131 | padding: $padding-base $padding-lg !important; 132 | .tags span { 133 | margin-right: $padding-sm !important; 134 | } 135 | } 136 | 137 | .compact { 138 | width: 140px; 139 | } 140 | 141 | .dot { 142 | width: 30px; 143 | overflow: hidden; 144 | padding: $padding-sm; 145 | 146 | .tags span { 147 | margin-right: 1px; 148 | } 149 | 150 | .node-bar-container { 151 | margin-top: $padding-sm; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /app/assets/sass/_plan.scss: -------------------------------------------------------------------------------- 1 | $connector-height: 12px; 2 | $connector-line: 2px solid darken($line-color, 10%); 3 | 4 | .plan { 5 | padding-bottom: $padding-lg * 3; 6 | margin-left: 100px; 7 | 8 | ul { 9 | display: flex; 10 | padding-top: $connector-height; 11 | position: relative; 12 | margin: auto; 13 | transition: all 0.5s; 14 | margin-top: -5px; 15 | 16 | // vertical connector 17 | ul::before { 18 | content: ''; 19 | position: absolute; top: 0; left: 50%; 20 | border-left: $connector-line; 21 | height: $connector-height; 22 | width: 0; 23 | } 24 | 25 | li { 26 | float: left; text-align: center; 27 | list-style-type: none; 28 | position: relative; 29 | padding: $connector-height $padding-sm 0 $padding-sm; 30 | transition: all 0.5s; 31 | 32 | // connectors 33 | &:before, &:after { 34 | content: ''; 35 | position: absolute; top: 0; right: 50%; 36 | border-top: $connector-line; 37 | width: 50%; height: $connector-height; 38 | } 39 | 40 | &:after { 41 | right: auto; left: 50%; 42 | border-left: $connector-line; 43 | } 44 | 45 | &:only-child { 46 | padding-top: 0; 47 | &:after, &:before { 48 | display: none; 49 | } 50 | } 51 | 52 | &:first-child::before, &:last-child::after { 53 | border: 0 none; 54 | } 55 | 56 | &:last-child::before { 57 | border-right: $connector-line; 58 | border-radius: 0 $border-radius-lg 0 0; 59 | } 60 | 61 | &:first-child::after { 62 | border-radius: $border-radius-lg 0 0 0; 63 | } 64 | 65 | //hovers 66 | .plan-node:hover+ul::before { 67 | border-color: $highlight-color; 68 | } 69 | 70 | .plan-node:hover+ul li::after, 71 | .plan-node:hover+ul li::before, 72 | .plan-node:hover+ul ul::before{ 73 | border-color: $highlight-color-dark; 74 | } 75 | } 76 | } 77 | } 78 | 79 | .plan-stats { 80 | display: flex; 81 | font-size: $font-size-base; 82 | margin: $padding-lg auto $padding-lg auto; 83 | padding-bottom: $padding-lg; 84 | border-bottom: 1px solid $line-color; 85 | border-radius: $border-radius-lg*2; 86 | width: 650px; 87 | position: relative; 88 | 89 | div { 90 | padding-right: $padding-lg; 91 | flex-grow: 1; 92 | } 93 | 94 | .stat-value { 95 | display: block; 96 | text-align: center; 97 | font-size: $font-size-lg; 98 | } 99 | 100 | .stat-label { 101 | display: block; 102 | text-align: center; 103 | font-size: $font-size-sm; 104 | } 105 | 106 | $triangle-size: 9px; 107 | &:after { 108 | content:''; 109 | position: absolute; 110 | top: 100%; 111 | left: 50%; 112 | margin-left: $triangle-size*-1; 113 | width: 0; 114 | height: 0; 115 | border-top: solid $triangle-size $line-color; 116 | border-left: solid $triangle-size transparent; 117 | border-right: solid $triangle-size transparent; 118 | } 119 | 120 | .btn-close { 121 | padding: $padding-base; 122 | background-color: transparent; 123 | font-size: $font-size-lg; 124 | text-align: center; 125 | @extend .text-muted; 126 | margin-left: $padding-base; 127 | cursor: pointer; 128 | border: 0; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /app/assets/sass/_table.scss: -------------------------------------------------------------------------------- 1 | .table { 2 | width: 100%; 3 | 4 | td { 5 | border-bottom: 1px solid $line-color; 6 | padding: $padding-base; 7 | } 8 | 9 | tr:hover { 10 | background-color: $gray-lightest; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/assets/sass/_variables.scss: -------------------------------------------------------------------------------- 1 | //vars 2 | $page-width: 1000px; 3 | 4 | $padding-base: 6px; 5 | $padding-sm: 3px; 6 | $padding-lg: 10px; 7 | $padding-xl: 18px; 8 | 9 | $font-size-base: 13px; 10 | $font-size-xs: round($font-size-base * 0.7); 11 | $font-size-sm: round($font-size-base * 0.9); 12 | $font-size-lg: round($font-size-base * 1.3); 13 | $font-size-xl: round($font-size-base * 1.7); 14 | 15 | $font-family-sans-serif: 'noto'; 16 | $font-family-mono: 'source code'; 17 | 18 | $line-height-base: 1.3; 19 | 20 | $gray-lightest: #f7f7f7; 21 | $gray-light: darken($gray-lightest, 10%); 22 | $gray: darken(#f7f7f7, 30%); 23 | $gray-dark: darken(#f7f7f7, 50%); 24 | $gray-darkest: darken($gray-lightest, 70%); 25 | 26 | $blue: #00B5E2; 27 | $dark-blue: #008CAF; 28 | $light-blue: #65DDFB; 29 | 30 | $red: #AF2F11; 31 | $dark-red: #7C210C; 32 | $light-red: #FB8165; 33 | 34 | $green: #279404; 35 | $yellow: #F8E400; 36 | 37 | $bg-color: $gray-lightest; 38 | 39 | $text-color: #4d525a; 40 | $text-color-light: lighten($text-color, 30%); 41 | 42 | $line-color: $gray-light; 43 | $line-color-light: lighten($gray-light, 10%); 44 | 45 | $link-color: $blue; 46 | 47 | $border-radius-base: 3px; 48 | $border-radius-lg: 6px; 49 | 50 | $main-color: $blue; 51 | $main-color-dark: $blue; 52 | 53 | $highlight-color: $blue; 54 | $highlight-color-dark: $dark-blue; 55 | 56 | $alert-color: #FB4418; 57 | -------------------------------------------------------------------------------- /app/assets/sass/font-awesome/_animated.scss: -------------------------------------------------------------------------------- 1 | // Spinning Icons 2 | // -------------------------- 3 | 4 | .#{$fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | .#{$fa-css-prefix}-pulse { 10 | -webkit-animation: fa-spin 1s infinite steps(8); 11 | animation: fa-spin 1s infinite steps(8); 12 | } 13 | 14 | @-webkit-keyframes fa-spin { 15 | 0% { 16 | -webkit-transform: rotate(0deg); 17 | transform: rotate(0deg); 18 | } 19 | 100% { 20 | -webkit-transform: rotate(359deg); 21 | transform: rotate(359deg); 22 | } 23 | } 24 | 25 | @keyframes fa-spin { 26 | 0% { 27 | -webkit-transform: rotate(0deg); 28 | transform: rotate(0deg); 29 | } 30 | 100% { 31 | -webkit-transform: rotate(359deg); 32 | transform: rotate(359deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/assets/sass/font-awesome/_bordered-pulled.scss: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em $fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .#{$fa-css-prefix}-pull-left { float: left; } 11 | .#{$fa-css-prefix}-pull-right { float: right; } 12 | 13 | .#{$fa-css-prefix} { 14 | &.#{$fa-css-prefix}-pull-left { margin-right: .3em; } 15 | &.#{$fa-css-prefix}-pull-right { margin-left: .3em; } 16 | } 17 | 18 | /* Deprecated as of 4.4.0 */ 19 | .pull-right { float: right; } 20 | .pull-left { float: left; } 21 | 22 | .#{$fa-css-prefix} { 23 | &.pull-left { margin-right: .3em; } 24 | &.pull-right { margin-left: .3em; } 25 | } 26 | -------------------------------------------------------------------------------- /app/assets/sass/font-awesome/_core.scss: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /app/assets/sass/font-awesome/_fixed-width.scss: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .#{$fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /app/assets/sass/font-awesome/_larger.scss: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .#{$fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .#{$fa-css-prefix}-2x { font-size: 2em; } 11 | .#{$fa-css-prefix}-3x { font-size: 3em; } 12 | .#{$fa-css-prefix}-4x { font-size: 4em; } 13 | .#{$fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /app/assets/sass/font-awesome/_list.scss: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: $fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .#{$fa-css-prefix}-li { 11 | position: absolute; 12 | left: -$fa-li-width; 13 | width: $fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.#{$fa-css-prefix}-lg { 17 | left: -$fa-li-width + (4em / 14); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /app/assets/sass/font-awesome/_mixins.scss: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | @mixin fa-icon() { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | 14 | @mixin fa-icon-rotate($degrees, $rotation) { 15 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}); 16 | -webkit-transform: rotate($degrees); 17 | -ms-transform: rotate($degrees); 18 | transform: rotate($degrees); 19 | } 20 | 21 | @mixin fa-icon-flip($horiz, $vert, $rotation) { 22 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}); 23 | -webkit-transform: scale($horiz, $vert); 24 | -ms-transform: scale($horiz, $vert); 25 | transform: scale($horiz, $vert); 26 | } 27 | -------------------------------------------------------------------------------- /app/assets/sass/font-awesome/_path.scss: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?v=#{$fa-version}'); 7 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}') format('embedded-opentype'), 8 | url('#{$fa-font-path}/fontawesome-webfont.woff2?v=#{$fa-version}') format('woff2'), 9 | url('#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}') format('woff'), 10 | url('#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}') format('truetype'), 11 | url('#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular') format('svg'); 12 | // src: url('#{$fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | -------------------------------------------------------------------------------- /app/assets/sass/font-awesome/_rotated-flipped.scss: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-rotate-90 { @include fa-icon-rotate(90deg, 1); } 5 | .#{$fa-css-prefix}-rotate-180 { @include fa-icon-rotate(180deg, 2); } 6 | .#{$fa-css-prefix}-rotate-270 { @include fa-icon-rotate(270deg, 3); } 7 | 8 | .#{$fa-css-prefix}-flip-horizontal { @include fa-icon-flip(-1, 1, 0); } 9 | .#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(1, -1, 2); } 10 | 11 | // Hook for IE8-9 12 | // ------------------------- 13 | 14 | :root .#{$fa-css-prefix}-rotate-90, 15 | :root .#{$fa-css-prefix}-rotate-180, 16 | :root .#{$fa-css-prefix}-rotate-270, 17 | :root .#{$fa-css-prefix}-flip-horizontal, 18 | :root .#{$fa-css-prefix}-flip-vertical { 19 | filter: none; 20 | } 21 | -------------------------------------------------------------------------------- /app/assets/sass/font-awesome/_stacked.scss: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .#{$fa-css-prefix}-stack-1x, .#{$fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .#{$fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .#{$fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .#{$fa-css-prefix}-inverse { color: $fa-inverse; } 21 | -------------------------------------------------------------------------------- /app/assets/sass/font-awesome/_styles.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.5.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables"; 7 | @import "mixins"; 8 | @import "path"; 9 | @import "core"; 10 | @import "larger"; 11 | @import "fixed-width"; 12 | @import "list"; 13 | @import "bordered-pulled"; 14 | @import "animated"; 15 | @import "rotated-flipped"; 16 | @import "stacked"; 17 | @import "icons"; 18 | -------------------------------------------------------------------------------- /app/assets/sass/font-awesome/_variables.scss: -------------------------------------------------------------------------------- 1 | // Variables 2 | // -------------------------- 3 | 4 | $fa-font-path: "../fonts" !default; 5 | $fa-font-size-base: 14px !default; 6 | $fa-line-height-base: 1 !default; 7 | //$fa-font-path: "//netdna.bootstrapcdn.com/font-awesome/4.5.0/fonts" !default; // for referencing Bootstrap CDN font files directly 8 | $fa-css-prefix: fa !default; 9 | $fa-version: "4.5.0" !default; 10 | $fa-border-color: #eee !default; 11 | $fa-inverse: #fff !default; 12 | $fa-li-width: (30em / 14) !default; 13 | 14 | $fa-var-500px: "\f26e"; 15 | $fa-var-adjust: "\f042"; 16 | $fa-var-adn: "\f170"; 17 | $fa-var-align-center: "\f037"; 18 | $fa-var-align-justify: "\f039"; 19 | $fa-var-align-left: "\f036"; 20 | $fa-var-align-right: "\f038"; 21 | $fa-var-amazon: "\f270"; 22 | $fa-var-ambulance: "\f0f9"; 23 | $fa-var-anchor: "\f13d"; 24 | $fa-var-android: "\f17b"; 25 | $fa-var-angellist: "\f209"; 26 | $fa-var-angle-double-down: "\f103"; 27 | $fa-var-angle-double-left: "\f100"; 28 | $fa-var-angle-double-right: "\f101"; 29 | $fa-var-angle-double-up: "\f102"; 30 | $fa-var-angle-down: "\f107"; 31 | $fa-var-angle-left: "\f104"; 32 | $fa-var-angle-right: "\f105"; 33 | $fa-var-angle-up: "\f106"; 34 | $fa-var-apple: "\f179"; 35 | $fa-var-archive: "\f187"; 36 | $fa-var-area-chart: "\f1fe"; 37 | $fa-var-arrow-circle-down: "\f0ab"; 38 | $fa-var-arrow-circle-left: "\f0a8"; 39 | $fa-var-arrow-circle-o-down: "\f01a"; 40 | $fa-var-arrow-circle-o-left: "\f190"; 41 | $fa-var-arrow-circle-o-right: "\f18e"; 42 | $fa-var-arrow-circle-o-up: "\f01b"; 43 | $fa-var-arrow-circle-right: "\f0a9"; 44 | $fa-var-arrow-circle-up: "\f0aa"; 45 | $fa-var-arrow-down: "\f063"; 46 | $fa-var-arrow-left: "\f060"; 47 | $fa-var-arrow-right: "\f061"; 48 | $fa-var-arrow-up: "\f062"; 49 | $fa-var-arrows: "\f047"; 50 | $fa-var-arrows-alt: "\f0b2"; 51 | $fa-var-arrows-h: "\f07e"; 52 | $fa-var-arrows-v: "\f07d"; 53 | $fa-var-asterisk: "\f069"; 54 | $fa-var-at: "\f1fa"; 55 | $fa-var-automobile: "\f1b9"; 56 | $fa-var-backward: "\f04a"; 57 | $fa-var-balance-scale: "\f24e"; 58 | $fa-var-ban: "\f05e"; 59 | $fa-var-bank: "\f19c"; 60 | $fa-var-bar-chart: "\f080"; 61 | $fa-var-bar-chart-o: "\f080"; 62 | $fa-var-barcode: "\f02a"; 63 | $fa-var-bars: "\f0c9"; 64 | $fa-var-battery-0: "\f244"; 65 | $fa-var-battery-1: "\f243"; 66 | $fa-var-battery-2: "\f242"; 67 | $fa-var-battery-3: "\f241"; 68 | $fa-var-battery-4: "\f240"; 69 | $fa-var-battery-empty: "\f244"; 70 | $fa-var-battery-full: "\f240"; 71 | $fa-var-battery-half: "\f242"; 72 | $fa-var-battery-quarter: "\f243"; 73 | $fa-var-battery-three-quarters: "\f241"; 74 | $fa-var-bed: "\f236"; 75 | $fa-var-beer: "\f0fc"; 76 | $fa-var-behance: "\f1b4"; 77 | $fa-var-behance-square: "\f1b5"; 78 | $fa-var-bell: "\f0f3"; 79 | $fa-var-bell-o: "\f0a2"; 80 | $fa-var-bell-slash: "\f1f6"; 81 | $fa-var-bell-slash-o: "\f1f7"; 82 | $fa-var-bicycle: "\f206"; 83 | $fa-var-binoculars: "\f1e5"; 84 | $fa-var-birthday-cake: "\f1fd"; 85 | $fa-var-bitbucket: "\f171"; 86 | $fa-var-bitbucket-square: "\f172"; 87 | $fa-var-bitcoin: "\f15a"; 88 | $fa-var-black-tie: "\f27e"; 89 | $fa-var-bluetooth: "\f293"; 90 | $fa-var-bluetooth-b: "\f294"; 91 | $fa-var-bold: "\f032"; 92 | $fa-var-bolt: "\f0e7"; 93 | $fa-var-bomb: "\f1e2"; 94 | $fa-var-book: "\f02d"; 95 | $fa-var-bookmark: "\f02e"; 96 | $fa-var-bookmark-o: "\f097"; 97 | $fa-var-briefcase: "\f0b1"; 98 | $fa-var-btc: "\f15a"; 99 | $fa-var-bug: "\f188"; 100 | $fa-var-building: "\f1ad"; 101 | $fa-var-building-o: "\f0f7"; 102 | $fa-var-bullhorn: "\f0a1"; 103 | $fa-var-bullseye: "\f140"; 104 | $fa-var-bus: "\f207"; 105 | $fa-var-buysellads: "\f20d"; 106 | $fa-var-cab: "\f1ba"; 107 | $fa-var-calculator: "\f1ec"; 108 | $fa-var-calendar: "\f073"; 109 | $fa-var-calendar-check-o: "\f274"; 110 | $fa-var-calendar-minus-o: "\f272"; 111 | $fa-var-calendar-o: "\f133"; 112 | $fa-var-calendar-plus-o: "\f271"; 113 | $fa-var-calendar-times-o: "\f273"; 114 | $fa-var-camera: "\f030"; 115 | $fa-var-camera-retro: "\f083"; 116 | $fa-var-car: "\f1b9"; 117 | $fa-var-caret-down: "\f0d7"; 118 | $fa-var-caret-left: "\f0d9"; 119 | $fa-var-caret-right: "\f0da"; 120 | $fa-var-caret-square-o-down: "\f150"; 121 | $fa-var-caret-square-o-left: "\f191"; 122 | $fa-var-caret-square-o-right: "\f152"; 123 | $fa-var-caret-square-o-up: "\f151"; 124 | $fa-var-caret-up: "\f0d8"; 125 | $fa-var-cart-arrow-down: "\f218"; 126 | $fa-var-cart-plus: "\f217"; 127 | $fa-var-cc: "\f20a"; 128 | $fa-var-cc-amex: "\f1f3"; 129 | $fa-var-cc-diners-club: "\f24c"; 130 | $fa-var-cc-discover: "\f1f2"; 131 | $fa-var-cc-jcb: "\f24b"; 132 | $fa-var-cc-mastercard: "\f1f1"; 133 | $fa-var-cc-paypal: "\f1f4"; 134 | $fa-var-cc-stripe: "\f1f5"; 135 | $fa-var-cc-visa: "\f1f0"; 136 | $fa-var-certificate: "\f0a3"; 137 | $fa-var-chain: "\f0c1"; 138 | $fa-var-chain-broken: "\f127"; 139 | $fa-var-check: "\f00c"; 140 | $fa-var-check-circle: "\f058"; 141 | $fa-var-check-circle-o: "\f05d"; 142 | $fa-var-check-square: "\f14a"; 143 | $fa-var-check-square-o: "\f046"; 144 | $fa-var-chevron-circle-down: "\f13a"; 145 | $fa-var-chevron-circle-left: "\f137"; 146 | $fa-var-chevron-circle-right: "\f138"; 147 | $fa-var-chevron-circle-up: "\f139"; 148 | $fa-var-chevron-down: "\f078"; 149 | $fa-var-chevron-left: "\f053"; 150 | $fa-var-chevron-right: "\f054"; 151 | $fa-var-chevron-up: "\f077"; 152 | $fa-var-child: "\f1ae"; 153 | $fa-var-chrome: "\f268"; 154 | $fa-var-circle: "\f111"; 155 | $fa-var-circle-o: "\f10c"; 156 | $fa-var-circle-o-notch: "\f1ce"; 157 | $fa-var-circle-thin: "\f1db"; 158 | $fa-var-clipboard: "\f0ea"; 159 | $fa-var-clock-o: "\f017"; 160 | $fa-var-clone: "\f24d"; 161 | $fa-var-close: "\f00d"; 162 | $fa-var-cloud: "\f0c2"; 163 | $fa-var-cloud-download: "\f0ed"; 164 | $fa-var-cloud-upload: "\f0ee"; 165 | $fa-var-cny: "\f157"; 166 | $fa-var-code: "\f121"; 167 | $fa-var-code-fork: "\f126"; 168 | $fa-var-codepen: "\f1cb"; 169 | $fa-var-codiepie: "\f284"; 170 | $fa-var-coffee: "\f0f4"; 171 | $fa-var-cog: "\f013"; 172 | $fa-var-cogs: "\f085"; 173 | $fa-var-columns: "\f0db"; 174 | $fa-var-comment: "\f075"; 175 | $fa-var-comment-o: "\f0e5"; 176 | $fa-var-commenting: "\f27a"; 177 | $fa-var-commenting-o: "\f27b"; 178 | $fa-var-comments: "\f086"; 179 | $fa-var-comments-o: "\f0e6"; 180 | $fa-var-compass: "\f14e"; 181 | $fa-var-compress: "\f066"; 182 | $fa-var-connectdevelop: "\f20e"; 183 | $fa-var-contao: "\f26d"; 184 | $fa-var-copy: "\f0c5"; 185 | $fa-var-copyright: "\f1f9"; 186 | $fa-var-creative-commons: "\f25e"; 187 | $fa-var-credit-card: "\f09d"; 188 | $fa-var-credit-card-alt: "\f283"; 189 | $fa-var-crop: "\f125"; 190 | $fa-var-crosshairs: "\f05b"; 191 | $fa-var-css3: "\f13c"; 192 | $fa-var-cube: "\f1b2"; 193 | $fa-var-cubes: "\f1b3"; 194 | $fa-var-cut: "\f0c4"; 195 | $fa-var-cutlery: "\f0f5"; 196 | $fa-var-dashboard: "\f0e4"; 197 | $fa-var-dashcube: "\f210"; 198 | $fa-var-database: "\f1c0"; 199 | $fa-var-dedent: "\f03b"; 200 | $fa-var-delicious: "\f1a5"; 201 | $fa-var-desktop: "\f108"; 202 | $fa-var-deviantart: "\f1bd"; 203 | $fa-var-diamond: "\f219"; 204 | $fa-var-digg: "\f1a6"; 205 | $fa-var-dollar: "\f155"; 206 | $fa-var-dot-circle-o: "\f192"; 207 | $fa-var-download: "\f019"; 208 | $fa-var-dribbble: "\f17d"; 209 | $fa-var-dropbox: "\f16b"; 210 | $fa-var-drupal: "\f1a9"; 211 | $fa-var-edge: "\f282"; 212 | $fa-var-edit: "\f044"; 213 | $fa-var-eject: "\f052"; 214 | $fa-var-ellipsis-h: "\f141"; 215 | $fa-var-ellipsis-v: "\f142"; 216 | $fa-var-empire: "\f1d1"; 217 | $fa-var-envelope: "\f0e0"; 218 | $fa-var-envelope-o: "\f003"; 219 | $fa-var-envelope-square: "\f199"; 220 | $fa-var-eraser: "\f12d"; 221 | $fa-var-eur: "\f153"; 222 | $fa-var-euro: "\f153"; 223 | $fa-var-exchange: "\f0ec"; 224 | $fa-var-exclamation: "\f12a"; 225 | $fa-var-exclamation-circle: "\f06a"; 226 | $fa-var-exclamation-triangle: "\f071"; 227 | $fa-var-expand: "\f065"; 228 | $fa-var-expeditedssl: "\f23e"; 229 | $fa-var-external-link: "\f08e"; 230 | $fa-var-external-link-square: "\f14c"; 231 | $fa-var-eye: "\f06e"; 232 | $fa-var-eye-slash: "\f070"; 233 | $fa-var-eyedropper: "\f1fb"; 234 | $fa-var-facebook: "\f09a"; 235 | $fa-var-facebook-f: "\f09a"; 236 | $fa-var-facebook-official: "\f230"; 237 | $fa-var-facebook-square: "\f082"; 238 | $fa-var-fast-backward: "\f049"; 239 | $fa-var-fast-forward: "\f050"; 240 | $fa-var-fax: "\f1ac"; 241 | $fa-var-feed: "\f09e"; 242 | $fa-var-female: "\f182"; 243 | $fa-var-fighter-jet: "\f0fb"; 244 | $fa-var-file: "\f15b"; 245 | $fa-var-file-archive-o: "\f1c6"; 246 | $fa-var-file-audio-o: "\f1c7"; 247 | $fa-var-file-code-o: "\f1c9"; 248 | $fa-var-file-excel-o: "\f1c3"; 249 | $fa-var-file-image-o: "\f1c5"; 250 | $fa-var-file-movie-o: "\f1c8"; 251 | $fa-var-file-o: "\f016"; 252 | $fa-var-file-pdf-o: "\f1c1"; 253 | $fa-var-file-photo-o: "\f1c5"; 254 | $fa-var-file-picture-o: "\f1c5"; 255 | $fa-var-file-powerpoint-o: "\f1c4"; 256 | $fa-var-file-sound-o: "\f1c7"; 257 | $fa-var-file-text: "\f15c"; 258 | $fa-var-file-text-o: "\f0f6"; 259 | $fa-var-file-video-o: "\f1c8"; 260 | $fa-var-file-word-o: "\f1c2"; 261 | $fa-var-file-zip-o: "\f1c6"; 262 | $fa-var-files-o: "\f0c5"; 263 | $fa-var-film: "\f008"; 264 | $fa-var-filter: "\f0b0"; 265 | $fa-var-fire: "\f06d"; 266 | $fa-var-fire-extinguisher: "\f134"; 267 | $fa-var-firefox: "\f269"; 268 | $fa-var-flag: "\f024"; 269 | $fa-var-flag-checkered: "\f11e"; 270 | $fa-var-flag-o: "\f11d"; 271 | $fa-var-flash: "\f0e7"; 272 | $fa-var-flask: "\f0c3"; 273 | $fa-var-flickr: "\f16e"; 274 | $fa-var-floppy-o: "\f0c7"; 275 | $fa-var-folder: "\f07b"; 276 | $fa-var-folder-o: "\f114"; 277 | $fa-var-folder-open: "\f07c"; 278 | $fa-var-folder-open-o: "\f115"; 279 | $fa-var-font: "\f031"; 280 | $fa-var-fonticons: "\f280"; 281 | $fa-var-fort-awesome: "\f286"; 282 | $fa-var-forumbee: "\f211"; 283 | $fa-var-forward: "\f04e"; 284 | $fa-var-foursquare: "\f180"; 285 | $fa-var-frown-o: "\f119"; 286 | $fa-var-futbol-o: "\f1e3"; 287 | $fa-var-gamepad: "\f11b"; 288 | $fa-var-gavel: "\f0e3"; 289 | $fa-var-gbp: "\f154"; 290 | $fa-var-ge: "\f1d1"; 291 | $fa-var-gear: "\f013"; 292 | $fa-var-gears: "\f085"; 293 | $fa-var-genderless: "\f22d"; 294 | $fa-var-get-pocket: "\f265"; 295 | $fa-var-gg: "\f260"; 296 | $fa-var-gg-circle: "\f261"; 297 | $fa-var-gift: "\f06b"; 298 | $fa-var-git: "\f1d3"; 299 | $fa-var-git-square: "\f1d2"; 300 | $fa-var-github: "\f09b"; 301 | $fa-var-github-alt: "\f113"; 302 | $fa-var-github-square: "\f092"; 303 | $fa-var-gittip: "\f184"; 304 | $fa-var-glass: "\f000"; 305 | $fa-var-globe: "\f0ac"; 306 | $fa-var-google: "\f1a0"; 307 | $fa-var-google-plus: "\f0d5"; 308 | $fa-var-google-plus-square: "\f0d4"; 309 | $fa-var-google-wallet: "\f1ee"; 310 | $fa-var-graduation-cap: "\f19d"; 311 | $fa-var-gratipay: "\f184"; 312 | $fa-var-group: "\f0c0"; 313 | $fa-var-h-square: "\f0fd"; 314 | $fa-var-hacker-news: "\f1d4"; 315 | $fa-var-hand-grab-o: "\f255"; 316 | $fa-var-hand-lizard-o: "\f258"; 317 | $fa-var-hand-o-down: "\f0a7"; 318 | $fa-var-hand-o-left: "\f0a5"; 319 | $fa-var-hand-o-right: "\f0a4"; 320 | $fa-var-hand-o-up: "\f0a6"; 321 | $fa-var-hand-paper-o: "\f256"; 322 | $fa-var-hand-peace-o: "\f25b"; 323 | $fa-var-hand-pointer-o: "\f25a"; 324 | $fa-var-hand-rock-o: "\f255"; 325 | $fa-var-hand-scissors-o: "\f257"; 326 | $fa-var-hand-spock-o: "\f259"; 327 | $fa-var-hand-stop-o: "\f256"; 328 | $fa-var-hashtag: "\f292"; 329 | $fa-var-hdd-o: "\f0a0"; 330 | $fa-var-header: "\f1dc"; 331 | $fa-var-headphones: "\f025"; 332 | $fa-var-heart: "\f004"; 333 | $fa-var-heart-o: "\f08a"; 334 | $fa-var-heartbeat: "\f21e"; 335 | $fa-var-history: "\f1da"; 336 | $fa-var-home: "\f015"; 337 | $fa-var-hospital-o: "\f0f8"; 338 | $fa-var-hotel: "\f236"; 339 | $fa-var-hourglass: "\f254"; 340 | $fa-var-hourglass-1: "\f251"; 341 | $fa-var-hourglass-2: "\f252"; 342 | $fa-var-hourglass-3: "\f253"; 343 | $fa-var-hourglass-end: "\f253"; 344 | $fa-var-hourglass-half: "\f252"; 345 | $fa-var-hourglass-o: "\f250"; 346 | $fa-var-hourglass-start: "\f251"; 347 | $fa-var-houzz: "\f27c"; 348 | $fa-var-html5: "\f13b"; 349 | $fa-var-i-cursor: "\f246"; 350 | $fa-var-ils: "\f20b"; 351 | $fa-var-image: "\f03e"; 352 | $fa-var-inbox: "\f01c"; 353 | $fa-var-indent: "\f03c"; 354 | $fa-var-industry: "\f275"; 355 | $fa-var-info: "\f129"; 356 | $fa-var-info-circle: "\f05a"; 357 | $fa-var-inr: "\f156"; 358 | $fa-var-instagram: "\f16d"; 359 | $fa-var-institution: "\f19c"; 360 | $fa-var-internet-explorer: "\f26b"; 361 | $fa-var-intersex: "\f224"; 362 | $fa-var-ioxhost: "\f208"; 363 | $fa-var-italic: "\f033"; 364 | $fa-var-joomla: "\f1aa"; 365 | $fa-var-jpy: "\f157"; 366 | $fa-var-jsfiddle: "\f1cc"; 367 | $fa-var-key: "\f084"; 368 | $fa-var-keyboard-o: "\f11c"; 369 | $fa-var-krw: "\f159"; 370 | $fa-var-language: "\f1ab"; 371 | $fa-var-laptop: "\f109"; 372 | $fa-var-lastfm: "\f202"; 373 | $fa-var-lastfm-square: "\f203"; 374 | $fa-var-leaf: "\f06c"; 375 | $fa-var-leanpub: "\f212"; 376 | $fa-var-legal: "\f0e3"; 377 | $fa-var-lemon-o: "\f094"; 378 | $fa-var-level-down: "\f149"; 379 | $fa-var-level-up: "\f148"; 380 | $fa-var-life-bouy: "\f1cd"; 381 | $fa-var-life-buoy: "\f1cd"; 382 | $fa-var-life-ring: "\f1cd"; 383 | $fa-var-life-saver: "\f1cd"; 384 | $fa-var-lightbulb-o: "\f0eb"; 385 | $fa-var-line-chart: "\f201"; 386 | $fa-var-link: "\f0c1"; 387 | $fa-var-linkedin: "\f0e1"; 388 | $fa-var-linkedin-square: "\f08c"; 389 | $fa-var-linux: "\f17c"; 390 | $fa-var-list: "\f03a"; 391 | $fa-var-list-alt: "\f022"; 392 | $fa-var-list-ol: "\f0cb"; 393 | $fa-var-list-ul: "\f0ca"; 394 | $fa-var-location-arrow: "\f124"; 395 | $fa-var-lock: "\f023"; 396 | $fa-var-long-arrow-down: "\f175"; 397 | $fa-var-long-arrow-left: "\f177"; 398 | $fa-var-long-arrow-right: "\f178"; 399 | $fa-var-long-arrow-up: "\f176"; 400 | $fa-var-magic: "\f0d0"; 401 | $fa-var-magnet: "\f076"; 402 | $fa-var-mail-forward: "\f064"; 403 | $fa-var-mail-reply: "\f112"; 404 | $fa-var-mail-reply-all: "\f122"; 405 | $fa-var-male: "\f183"; 406 | $fa-var-map: "\f279"; 407 | $fa-var-map-marker: "\f041"; 408 | $fa-var-map-o: "\f278"; 409 | $fa-var-map-pin: "\f276"; 410 | $fa-var-map-signs: "\f277"; 411 | $fa-var-mars: "\f222"; 412 | $fa-var-mars-double: "\f227"; 413 | $fa-var-mars-stroke: "\f229"; 414 | $fa-var-mars-stroke-h: "\f22b"; 415 | $fa-var-mars-stroke-v: "\f22a"; 416 | $fa-var-maxcdn: "\f136"; 417 | $fa-var-meanpath: "\f20c"; 418 | $fa-var-medium: "\f23a"; 419 | $fa-var-medkit: "\f0fa"; 420 | $fa-var-meh-o: "\f11a"; 421 | $fa-var-mercury: "\f223"; 422 | $fa-var-microphone: "\f130"; 423 | $fa-var-microphone-slash: "\f131"; 424 | $fa-var-minus: "\f068"; 425 | $fa-var-minus-circle: "\f056"; 426 | $fa-var-minus-square: "\f146"; 427 | $fa-var-minus-square-o: "\f147"; 428 | $fa-var-mixcloud: "\f289"; 429 | $fa-var-mobile: "\f10b"; 430 | $fa-var-mobile-phone: "\f10b"; 431 | $fa-var-modx: "\f285"; 432 | $fa-var-money: "\f0d6"; 433 | $fa-var-moon-o: "\f186"; 434 | $fa-var-mortar-board: "\f19d"; 435 | $fa-var-motorcycle: "\f21c"; 436 | $fa-var-mouse-pointer: "\f245"; 437 | $fa-var-music: "\f001"; 438 | $fa-var-navicon: "\f0c9"; 439 | $fa-var-neuter: "\f22c"; 440 | $fa-var-newspaper-o: "\f1ea"; 441 | $fa-var-object-group: "\f247"; 442 | $fa-var-object-ungroup: "\f248"; 443 | $fa-var-odnoklassniki: "\f263"; 444 | $fa-var-odnoklassniki-square: "\f264"; 445 | $fa-var-opencart: "\f23d"; 446 | $fa-var-openid: "\f19b"; 447 | $fa-var-opera: "\f26a"; 448 | $fa-var-optin-monster: "\f23c"; 449 | $fa-var-outdent: "\f03b"; 450 | $fa-var-pagelines: "\f18c"; 451 | $fa-var-paint-brush: "\f1fc"; 452 | $fa-var-paper-plane: "\f1d8"; 453 | $fa-var-paper-plane-o: "\f1d9"; 454 | $fa-var-paperclip: "\f0c6"; 455 | $fa-var-paragraph: "\f1dd"; 456 | $fa-var-paste: "\f0ea"; 457 | $fa-var-pause: "\f04c"; 458 | $fa-var-pause-circle: "\f28b"; 459 | $fa-var-pause-circle-o: "\f28c"; 460 | $fa-var-paw: "\f1b0"; 461 | $fa-var-paypal: "\f1ed"; 462 | $fa-var-pencil: "\f040"; 463 | $fa-var-pencil-square: "\f14b"; 464 | $fa-var-pencil-square-o: "\f044"; 465 | $fa-var-percent: "\f295"; 466 | $fa-var-phone: "\f095"; 467 | $fa-var-phone-square: "\f098"; 468 | $fa-var-photo: "\f03e"; 469 | $fa-var-picture-o: "\f03e"; 470 | $fa-var-pie-chart: "\f200"; 471 | $fa-var-pied-piper: "\f1a7"; 472 | $fa-var-pied-piper-alt: "\f1a8"; 473 | $fa-var-pinterest: "\f0d2"; 474 | $fa-var-pinterest-p: "\f231"; 475 | $fa-var-pinterest-square: "\f0d3"; 476 | $fa-var-plane: "\f072"; 477 | $fa-var-play: "\f04b"; 478 | $fa-var-play-circle: "\f144"; 479 | $fa-var-play-circle-o: "\f01d"; 480 | $fa-var-plug: "\f1e6"; 481 | $fa-var-plus: "\f067"; 482 | $fa-var-plus-circle: "\f055"; 483 | $fa-var-plus-square: "\f0fe"; 484 | $fa-var-plus-square-o: "\f196"; 485 | $fa-var-power-off: "\f011"; 486 | $fa-var-print: "\f02f"; 487 | $fa-var-product-hunt: "\f288"; 488 | $fa-var-puzzle-piece: "\f12e"; 489 | $fa-var-qq: "\f1d6"; 490 | $fa-var-qrcode: "\f029"; 491 | $fa-var-question: "\f128"; 492 | $fa-var-question-circle: "\f059"; 493 | $fa-var-quote-left: "\f10d"; 494 | $fa-var-quote-right: "\f10e"; 495 | $fa-var-ra: "\f1d0"; 496 | $fa-var-random: "\f074"; 497 | $fa-var-rebel: "\f1d0"; 498 | $fa-var-recycle: "\f1b8"; 499 | $fa-var-reddit: "\f1a1"; 500 | $fa-var-reddit-alien: "\f281"; 501 | $fa-var-reddit-square: "\f1a2"; 502 | $fa-var-refresh: "\f021"; 503 | $fa-var-registered: "\f25d"; 504 | $fa-var-remove: "\f00d"; 505 | $fa-var-renren: "\f18b"; 506 | $fa-var-reorder: "\f0c9"; 507 | $fa-var-repeat: "\f01e"; 508 | $fa-var-reply: "\f112"; 509 | $fa-var-reply-all: "\f122"; 510 | $fa-var-retweet: "\f079"; 511 | $fa-var-rmb: "\f157"; 512 | $fa-var-road: "\f018"; 513 | $fa-var-rocket: "\f135"; 514 | $fa-var-rotate-left: "\f0e2"; 515 | $fa-var-rotate-right: "\f01e"; 516 | $fa-var-rouble: "\f158"; 517 | $fa-var-rss: "\f09e"; 518 | $fa-var-rss-square: "\f143"; 519 | $fa-var-rub: "\f158"; 520 | $fa-var-ruble: "\f158"; 521 | $fa-var-rupee: "\f156"; 522 | $fa-var-safari: "\f267"; 523 | $fa-var-save: "\f0c7"; 524 | $fa-var-scissors: "\f0c4"; 525 | $fa-var-scribd: "\f28a"; 526 | $fa-var-search: "\f002"; 527 | $fa-var-search-minus: "\f010"; 528 | $fa-var-search-plus: "\f00e"; 529 | $fa-var-sellsy: "\f213"; 530 | $fa-var-send: "\f1d8"; 531 | $fa-var-send-o: "\f1d9"; 532 | $fa-var-server: "\f233"; 533 | $fa-var-share: "\f064"; 534 | $fa-var-share-alt: "\f1e0"; 535 | $fa-var-share-alt-square: "\f1e1"; 536 | $fa-var-share-square: "\f14d"; 537 | $fa-var-share-square-o: "\f045"; 538 | $fa-var-shekel: "\f20b"; 539 | $fa-var-sheqel: "\f20b"; 540 | $fa-var-shield: "\f132"; 541 | $fa-var-ship: "\f21a"; 542 | $fa-var-shirtsinbulk: "\f214"; 543 | $fa-var-shopping-bag: "\f290"; 544 | $fa-var-shopping-basket: "\f291"; 545 | $fa-var-shopping-cart: "\f07a"; 546 | $fa-var-sign-in: "\f090"; 547 | $fa-var-sign-out: "\f08b"; 548 | $fa-var-signal: "\f012"; 549 | $fa-var-simplybuilt: "\f215"; 550 | $fa-var-sitemap: "\f0e8"; 551 | $fa-var-skyatlas: "\f216"; 552 | $fa-var-skype: "\f17e"; 553 | $fa-var-slack: "\f198"; 554 | $fa-var-sliders: "\f1de"; 555 | $fa-var-slideshare: "\f1e7"; 556 | $fa-var-smile-o: "\f118"; 557 | $fa-var-soccer-ball-o: "\f1e3"; 558 | $fa-var-sort: "\f0dc"; 559 | $fa-var-sort-alpha-asc: "\f15d"; 560 | $fa-var-sort-alpha-desc: "\f15e"; 561 | $fa-var-sort-amount-asc: "\f160"; 562 | $fa-var-sort-amount-desc: "\f161"; 563 | $fa-var-sort-asc: "\f0de"; 564 | $fa-var-sort-desc: "\f0dd"; 565 | $fa-var-sort-down: "\f0dd"; 566 | $fa-var-sort-numeric-asc: "\f162"; 567 | $fa-var-sort-numeric-desc: "\f163"; 568 | $fa-var-sort-up: "\f0de"; 569 | $fa-var-soundcloud: "\f1be"; 570 | $fa-var-space-shuttle: "\f197"; 571 | $fa-var-spinner: "\f110"; 572 | $fa-var-spoon: "\f1b1"; 573 | $fa-var-spotify: "\f1bc"; 574 | $fa-var-square: "\f0c8"; 575 | $fa-var-square-o: "\f096"; 576 | $fa-var-stack-exchange: "\f18d"; 577 | $fa-var-stack-overflow: "\f16c"; 578 | $fa-var-star: "\f005"; 579 | $fa-var-star-half: "\f089"; 580 | $fa-var-star-half-empty: "\f123"; 581 | $fa-var-star-half-full: "\f123"; 582 | $fa-var-star-half-o: "\f123"; 583 | $fa-var-star-o: "\f006"; 584 | $fa-var-steam: "\f1b6"; 585 | $fa-var-steam-square: "\f1b7"; 586 | $fa-var-step-backward: "\f048"; 587 | $fa-var-step-forward: "\f051"; 588 | $fa-var-stethoscope: "\f0f1"; 589 | $fa-var-sticky-note: "\f249"; 590 | $fa-var-sticky-note-o: "\f24a"; 591 | $fa-var-stop: "\f04d"; 592 | $fa-var-stop-circle: "\f28d"; 593 | $fa-var-stop-circle-o: "\f28e"; 594 | $fa-var-street-view: "\f21d"; 595 | $fa-var-strikethrough: "\f0cc"; 596 | $fa-var-stumbleupon: "\f1a4"; 597 | $fa-var-stumbleupon-circle: "\f1a3"; 598 | $fa-var-subscript: "\f12c"; 599 | $fa-var-subway: "\f239"; 600 | $fa-var-suitcase: "\f0f2"; 601 | $fa-var-sun-o: "\f185"; 602 | $fa-var-superscript: "\f12b"; 603 | $fa-var-support: "\f1cd"; 604 | $fa-var-table: "\f0ce"; 605 | $fa-var-tablet: "\f10a"; 606 | $fa-var-tachometer: "\f0e4"; 607 | $fa-var-tag: "\f02b"; 608 | $fa-var-tags: "\f02c"; 609 | $fa-var-tasks: "\f0ae"; 610 | $fa-var-taxi: "\f1ba"; 611 | $fa-var-television: "\f26c"; 612 | $fa-var-tencent-weibo: "\f1d5"; 613 | $fa-var-terminal: "\f120"; 614 | $fa-var-text-height: "\f034"; 615 | $fa-var-text-width: "\f035"; 616 | $fa-var-th: "\f00a"; 617 | $fa-var-th-large: "\f009"; 618 | $fa-var-th-list: "\f00b"; 619 | $fa-var-thumb-tack: "\f08d"; 620 | $fa-var-thumbs-down: "\f165"; 621 | $fa-var-thumbs-o-down: "\f088"; 622 | $fa-var-thumbs-o-up: "\f087"; 623 | $fa-var-thumbs-up: "\f164"; 624 | $fa-var-ticket: "\f145"; 625 | $fa-var-times: "\f00d"; 626 | $fa-var-times-circle: "\f057"; 627 | $fa-var-times-circle-o: "\f05c"; 628 | $fa-var-tint: "\f043"; 629 | $fa-var-toggle-down: "\f150"; 630 | $fa-var-toggle-left: "\f191"; 631 | $fa-var-toggle-off: "\f204"; 632 | $fa-var-toggle-on: "\f205"; 633 | $fa-var-toggle-right: "\f152"; 634 | $fa-var-toggle-up: "\f151"; 635 | $fa-var-trademark: "\f25c"; 636 | $fa-var-train: "\f238"; 637 | $fa-var-transgender: "\f224"; 638 | $fa-var-transgender-alt: "\f225"; 639 | $fa-var-trash: "\f1f8"; 640 | $fa-var-trash-o: "\f014"; 641 | $fa-var-tree: "\f1bb"; 642 | $fa-var-trello: "\f181"; 643 | $fa-var-tripadvisor: "\f262"; 644 | $fa-var-trophy: "\f091"; 645 | $fa-var-truck: "\f0d1"; 646 | $fa-var-try: "\f195"; 647 | $fa-var-tty: "\f1e4"; 648 | $fa-var-tumblr: "\f173"; 649 | $fa-var-tumblr-square: "\f174"; 650 | $fa-var-turkish-lira: "\f195"; 651 | $fa-var-tv: "\f26c"; 652 | $fa-var-twitch: "\f1e8"; 653 | $fa-var-twitter: "\f099"; 654 | $fa-var-twitter-square: "\f081"; 655 | $fa-var-umbrella: "\f0e9"; 656 | $fa-var-underline: "\f0cd"; 657 | $fa-var-undo: "\f0e2"; 658 | $fa-var-university: "\f19c"; 659 | $fa-var-unlink: "\f127"; 660 | $fa-var-unlock: "\f09c"; 661 | $fa-var-unlock-alt: "\f13e"; 662 | $fa-var-unsorted: "\f0dc"; 663 | $fa-var-upload: "\f093"; 664 | $fa-var-usb: "\f287"; 665 | $fa-var-usd: "\f155"; 666 | $fa-var-user: "\f007"; 667 | $fa-var-user-md: "\f0f0"; 668 | $fa-var-user-plus: "\f234"; 669 | $fa-var-user-secret: "\f21b"; 670 | $fa-var-user-times: "\f235"; 671 | $fa-var-users: "\f0c0"; 672 | $fa-var-venus: "\f221"; 673 | $fa-var-venus-double: "\f226"; 674 | $fa-var-venus-mars: "\f228"; 675 | $fa-var-viacoin: "\f237"; 676 | $fa-var-video-camera: "\f03d"; 677 | $fa-var-vimeo: "\f27d"; 678 | $fa-var-vimeo-square: "\f194"; 679 | $fa-var-vine: "\f1ca"; 680 | $fa-var-vk: "\f189"; 681 | $fa-var-volume-down: "\f027"; 682 | $fa-var-volume-off: "\f026"; 683 | $fa-var-volume-up: "\f028"; 684 | $fa-var-warning: "\f071"; 685 | $fa-var-wechat: "\f1d7"; 686 | $fa-var-weibo: "\f18a"; 687 | $fa-var-weixin: "\f1d7"; 688 | $fa-var-whatsapp: "\f232"; 689 | $fa-var-wheelchair: "\f193"; 690 | $fa-var-wifi: "\f1eb"; 691 | $fa-var-wikipedia-w: "\f266"; 692 | $fa-var-windows: "\f17a"; 693 | $fa-var-won: "\f159"; 694 | $fa-var-wordpress: "\f19a"; 695 | $fa-var-wrench: "\f0ad"; 696 | $fa-var-xing: "\f168"; 697 | $fa-var-xing-square: "\f169"; 698 | $fa-var-y-combinator: "\f23b"; 699 | $fa-var-y-combinator-square: "\f1d4"; 700 | $fa-var-yahoo: "\f19e"; 701 | $fa-var-yc: "\f23b"; 702 | $fa-var-yc-square: "\f1d4"; 703 | $fa-var-yelp: "\f1e9"; 704 | $fa-var-yen: "\f157"; 705 | $fa-var-youtube: "\f167"; 706 | $fa-var-youtube-play: "\f16a"; 707 | $fa-var-youtube-square: "\f166"; 708 | 709 | -------------------------------------------------------------------------------- /app/assets/sass/styles.scss: -------------------------------------------------------------------------------- 1 | @import "compass/reset"; 2 | @import "font-awesome/styles"; 3 | 4 | @import "variables"; 5 | @import "fonts"; 6 | @import "common"; 7 | @import "buttons"; 8 | @import "forms"; 9 | @import "nav"; 10 | 11 | @import "plan"; 12 | @import "plan-node"; 13 | @import "menu"; 14 | @import "page"; 15 | @import "table"; 16 | @import "modal"; 17 | @import "footer"; 18 | @import "highlight"; 19 | -------------------------------------------------------------------------------- /app/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import {provide} from 'angular2/core'; 2 | import {bootstrap} from 'angular2/platform/browser'; 3 | import {ROUTER_PROVIDERS, LocationStrategy, HashLocationStrategy} from 'angular2/router'; 4 | import {App} from './components/app/app'; 5 | 6 | bootstrap(App, [ 7 | ROUTER_PROVIDERS, 8 | provide(LocationStrategy, { useClass: HashLocationStrategy }) 9 | ]); 10 | -------------------------------------------------------------------------------- /app/components/about/about.html: -------------------------------------------------------------------------------- 1 |
2 |

Postgres EXPLAIN Visualizer (Pev)

3 |

4 | Pev is designed to make 5 | Postgres query plans easier to grok. It displays a plan as a tree, with each node representing a step that takes in a row set 6 | and produces another. Pev can show you a number of useful things:

7 | 15 | 16 |

You can tweak display options using the menu on the right.

17 | 18 |

Usage tips

19 |

Pev currently accepts only JSON formatted plans. In fact, the get the most out of it, 20 | I recommend generating a query plan using the following line: 21 | EXPLAIN (ANALYZE, COSTS, VERBOSE, BUFFERS, FORMAT JSON). I also recommend submitting a (decently formatted) 22 | SQL query that generated the plan. Doing so will make Pev more useful. 23 |

24 | 25 |

Pev will remember the plans you analyzed. They are stored locally and are not submitted to me. This is good 26 | because no one but you will see your queries. It's also bad because you can't share them with others.

27 | 28 |

Acknowledgements

29 |

Pev was inspired and heavily influenced by the excellent explain.depesz.com. Both the 30 | tool and the corresponding help files are a great resource to learn about Postgres and its planner. 31 |

32 | 33 |

Help me improve Pev

34 |

If you want to help, there are multiple ways to contribute:

35 | 40 | 41 |
42 | -------------------------------------------------------------------------------- /app/components/about/about.ts: -------------------------------------------------------------------------------- 1 | import {Component} from 'angular2/core'; 2 | 3 | @Component({ 4 | selector: 'about', 5 | templateUrl: './components/about/about.html' 6 | }) 7 | export class About {} 8 | -------------------------------------------------------------------------------- /app/components/app/app.html: -------------------------------------------------------------------------------- 1 | 8 | 9 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /app/components/app/app.ts: -------------------------------------------------------------------------------- 1 | import {Component, ViewEncapsulation} from 'angular2/core'; 2 | import {RouteConfig, ROUTER_DIRECTIVES} from 'angular2/router'; 3 | 4 | import {PlanView} from '../plan-view/plan-view'; 5 | import {PlanList} from '../plan-list/plan-list'; 6 | import {PlanNew} from '../plan-new/plan-new'; 7 | import {About} from '../about/about'; 8 | 9 | @Component({ 10 | selector: 'app', 11 | templateUrl: './components/app/app.html', 12 | encapsulation: ViewEncapsulation.None, 13 | directives: [ROUTER_DIRECTIVES] 14 | }) 15 | 16 | @RouteConfig([ 17 | { path: '/', redirectTo: ['/PlanList'] }, 18 | { path: '/plans', component: PlanList, name: 'PlanList' }, 19 | { path: '/plans/new', component: PlanNew, name: 'PlanNew' }, 20 | { path: '/plans/:id', component: PlanView, name: 'PlanView' }, 21 | { path: '/about', component: About, name: 'About'} 22 | ]) 23 | 24 | export class App { } 25 | -------------------------------------------------------------------------------- /app/components/plan-list/plan-list.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | Welcome to PEV! Please submit a plan for visualization 4 |
5 | 6 | 7 | 8 | 9 |
10 | 11 | 22 |
23 | 24 | 25 | 26 | 29 | 30 |
{{plan.name}}created on {{plan.createdOn | momentDate }} 28 |
31 |
32 | -------------------------------------------------------------------------------- /app/components/plan-list/plan-list.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from 'angular2/core'; 2 | import {ROUTER_DIRECTIVES} from 'angular2/router'; 3 | 4 | import {IPlan} from '../../interfaces/iplan'; 5 | import {PlanService} from '../../services/plan-service'; 6 | import {PlanNew} from '../plan-new/plan-new'; 7 | 8 | import {MomentDatePipe} from '../../pipes'; 9 | 10 | @Component({ 11 | selector: 'plan-list', 12 | templateUrl: './components/plan-list/plan-list.html', 13 | providers: [PlanService], 14 | directives: [ROUTER_DIRECTIVES, PlanNew], 15 | pipes: [MomentDatePipe] 16 | }) 17 | export class PlanList { 18 | plans: Array; 19 | newPlanName: string; 20 | newPlanContent: any; 21 | newPlanId: string; 22 | openDialog: boolean = false; 23 | planToDelete: IPlan; 24 | 25 | constructor(private _planService: PlanService) { } 26 | 27 | ngOnInit() { 28 | this.plans = this._planService.getPlans(); 29 | } 30 | 31 | requestDelete(plan) { 32 | this.openDialog = true; 33 | this.planToDelete = plan; 34 | } 35 | 36 | deletePlan(plan) { 37 | this.openDialog = false; 38 | console.log(this.planToDelete); 39 | this._planService.deletePlan(this.planToDelete); 40 | this.plans = this._planService.getPlans(); 41 | } 42 | 43 | cancelDelete() { 44 | this.openDialog = false; 45 | } 46 | 47 | deleteAllPlans() { 48 | this._planService.deleteAllPlans(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/components/plan-new/plan-new.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | For best results, use EXPLAIN (ANALYZE, COSTS, VERBOSE, BUFFERS, FORMAT JSON)
5 | Psql users can export the plan to a file using psql -qAt -f explain.sql > analyze.json
6 |

DISCLAIMER: Pev stores your plans locally (localStorage) and will not send them anywhere.

7 |
8 | 9 | 10 |
11 | 12 |

{{validationMessage}}

13 | 14 | 15 |
16 | -------------------------------------------------------------------------------- /app/components/plan-new/plan-new.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from 'angular2/core'; 2 | import {Router, ROUTER_DIRECTIVES} from 'angular2/router'; 3 | import {IPlan} from '../../interfaces/iplan'; 4 | 5 | import {PlanService} from '../../services/plan-service'; 6 | 7 | @Component({ 8 | selector: 'plan-new', 9 | templateUrl: './components/plan-new/plan-new.html', 10 | providers: [PlanService], 11 | directives: [ROUTER_DIRECTIVES] 12 | }) 13 | export class PlanNew { 14 | planIds: string[]; 15 | newPlanName: string; 16 | newPlanContent: string; 17 | newPlanQuery: string; 18 | newPlan: IPlan; 19 | validationMessage: string; 20 | 21 | constructor(private _router: Router, private _planService: PlanService) { } 22 | 23 | submitPlan() { 24 | // remove psql generated header 25 | this.newPlanContent = this.newPlanContent.replace('QUERY PLAN', ''); 26 | 27 | if (!this._planService.isJsonString(this.newPlanContent)) { 28 | this.validationMessage = 'The string you submitted is not valid JSON' 29 | return; 30 | } 31 | 32 | this.newPlan = this._planService.createPlan(this.newPlanName, this.newPlanContent, this.newPlanQuery); 33 | this._router.navigate(['PlanView', { id: this.newPlan.id }]); 34 | } 35 | 36 | prefill() { 37 | this.newPlanName = 'Sample plan'; 38 | this.newPlanContent = SAMPLE_JSON; 39 | this.newPlanQuery = SAMPLE_QUERY; 40 | } 41 | } 42 | export var SAMPLE_JSON = `[ 43 | { 44 | "Plan": { 45 | "Node Type": "Limit", 46 | "Startup Cost": 17024.84, 47 | "Total Cost": 17024.87, 48 | "Plan Rows": 10, 49 | "Plan Width": 133, 50 | "Actual Startup Time": 725.773, 51 | "Actual Total Time": 725.775, 52 | "Actual Rows": 10, 53 | "Actual Loops": 1, 54 | "Output": ["c.state", "cat.categoryname", "(sum(o.netamount))", "(sum(o.totalamount))"], 55 | "Shared Hit Blocks": 23, 56 | "Shared Read Blocks": 1392, 57 | "Shared Dirtied Blocks": 0, 58 | "Shared Written Blocks": 0, 59 | "Local Hit Blocks": 0, 60 | "Local Read Blocks": 0, 61 | "Local Dirtied Blocks": 0, 62 | "Local Written Blocks": 0, 63 | "Temp Read Blocks": 0, 64 | "Temp Written Blocks": 0, 65 | "I/O Read Time": 0.000, 66 | "I/O Write Time": 0.000, 67 | "Plans": [ 68 | { 69 | "Node Type": "Sort", 70 | "Parent Relationship": "Outer", 71 | "Startup Cost": 17024.84, 72 | "Total Cost": 17026.88, 73 | "Plan Rows": 816, 74 | "Plan Width": 133, 75 | "Actual Startup Time": 725.771, 76 | "Actual Total Time": 725.772, 77 | "Actual Rows": 11, 78 | "Actual Loops": 1, 79 | "Output": ["c.state", "cat.categoryname", "(sum(o.netamount))", "(sum(o.totalamount))"], 80 | "Sort Key": ["c.state", "(sum(o.totalamount))"], 81 | "Sort Method": "top-N heapsort", 82 | "Sort Space Used": 25, 83 | "Sort Space Type": "Memory", 84 | "Shared Hit Blocks": 23, 85 | "Shared Read Blocks": 1392, 86 | "Shared Dirtied Blocks": 0, 87 | "Shared Written Blocks": 0, 88 | "Local Hit Blocks": 0, 89 | "Local Read Blocks": 0, 90 | "Local Dirtied Blocks": 0, 91 | "Local Written Blocks": 0, 92 | "Temp Read Blocks": 0, 93 | "Temp Written Blocks": 0, 94 | "I/O Read Time": 0.000, 95 | "I/O Write Time": 0.000, 96 | "Plans": [ 97 | { 98 | "Node Type": "Aggregate", 99 | "Strategy": "Hashed", 100 | "Parent Relationship": "Outer", 101 | "Startup Cost": 16994.41, 102 | "Total Cost": 17006.65, 103 | "Plan Rows": 816, 104 | "Plan Width": 133, 105 | "Actual Startup Time": 723.877, 106 | "Actual Total Time": 724.417, 107 | "Actual Rows": 832, 108 | "Actual Loops": 1, 109 | "Output": ["c.state", "cat.categoryname", "sum(o.netamount)", "sum(o.totalamount)"], 110 | "Group Key": ["c.state", "cat.categoryname"], 111 | "Shared Hit Blocks": 13, 112 | "Shared Read Blocks": 1392, 113 | "Shared Dirtied Blocks": 0, 114 | "Shared Written Blocks": 0, 115 | "Local Hit Blocks": 0, 116 | "Local Read Blocks": 0, 117 | "Local Dirtied Blocks": 0, 118 | "Local Written Blocks": 0, 119 | "Temp Read Blocks": 0, 120 | "Temp Written Blocks": 0, 121 | "I/O Read Time": 0.000, 122 | "I/O Write Time": 0.000, 123 | "Plans": [ 124 | { 125 | "Node Type": "Hash Join", 126 | "Parent Relationship": "Outer", 127 | "Join Type": "Inner", 128 | "Startup Cost": 4966.48, 129 | "Total Cost": 13742.65, 130 | "Plan Rows": 325176, 131 | "Plan Width": 133, 132 | "Actual Startup Time": 118.314, 133 | "Actual Total Time": 354.285, 134 | "Actual Rows": 383270, 135 | "Actual Loops": 1, 136 | "Output": ["c.state", "o.netamount", "o.totalamount", "cat.categoryname"], 137 | "Hash Cond": "(o.orderid = ch.orderid)", 138 | "Shared Hit Blocks": 13, 139 | "Shared Read Blocks": 1392, 140 | "Shared Dirtied Blocks": 0, 141 | "Shared Written Blocks": 0, 142 | "Local Hit Blocks": 0, 143 | "Local Read Blocks": 0, 144 | "Local Dirtied Blocks": 0, 145 | "Local Written Blocks": 0, 146 | "Temp Read Blocks": 0, 147 | "Temp Written Blocks": 0, 148 | "I/O Read Time": 0.000, 149 | "I/O Write Time": 0.000, 150 | "Plans": [ 151 | { 152 | "Node Type": "Hash Join", 153 | "Parent Relationship": "Outer", 154 | "Join Type": "Inner", 155 | "Startup Cost": 834.86, 156 | "Total Cost": 4539.11, 157 | "Plan Rows": 60350, 158 | "Plan Width": 138, 159 | "Actual Startup Time": 22.651, 160 | "Actual Total Time": 133.484, 161 | "Actual Rows": 60350, 162 | "Actual Loops": 1, 163 | "Output": ["o.netamount", "o.totalamount", "o.orderid", "ol.orderid", "cat.categoryname"], 164 | "Hash Cond": "(ol.orderid = o.orderid)", 165 | "Shared Hit Blocks": 9, 166 | "Shared Read Blocks": 581, 167 | "Shared Dirtied Blocks": 0, 168 | "Shared Written Blocks": 0, 169 | "Local Hit Blocks": 0, 170 | "Local Read Blocks": 0, 171 | "Local Dirtied Blocks": 0, 172 | "Local Written Blocks": 0, 173 | "Temp Read Blocks": 0, 174 | "Temp Written Blocks": 0, 175 | "I/O Read Time": 0.000, 176 | "I/O Write Time": 0.000, 177 | "Plans": [ 178 | { 179 | "Node Type": "Hash Join", 180 | "Parent Relationship": "Outer", 181 | "Join Type": "Inner", 182 | "Startup Cost": 464.86, 183 | "Total Cost": 2962.11, 184 | "Plan Rows": 60350, 185 | "Plan Width": 122, 186 | "Actual Startup Time": 12.467, 187 | "Actual Total Time": 85.647, 188 | "Actual Rows": 60350, 189 | "Actual Loops": 1, 190 | "Output": ["ol.orderid", "cat.categoryname"], 191 | "Hash Cond": "(ol.prod_id = p.prod_id)", 192 | "Shared Hit Blocks": 4, 193 | "Shared Read Blocks": 483, 194 | "Shared Dirtied Blocks": 0, 195 | "Shared Written Blocks": 0, 196 | "Local Hit Blocks": 0, 197 | "Local Read Blocks": 0, 198 | "Local Dirtied Blocks": 0, 199 | "Local Written Blocks": 0, 200 | "Temp Read Blocks": 0, 201 | "Temp Written Blocks": 0, 202 | "I/O Read Time": 0.000, 203 | "I/O Write Time": 0.000, 204 | "Plans": [ 205 | { 206 | "Node Type": "Seq Scan", 207 | "Parent Relationship": "Outer", 208 | "Relation Name": "orderlines", 209 | "Schema": "public", 210 | "Alias": "ol", 211 | "Startup Cost": 0.00, 212 | "Total Cost": 988.50, 213 | "Plan Rows": 60350, 214 | "Plan Width": 8, 215 | "Actual Startup Time": 0.005, 216 | "Actual Total Time": 14.054, 217 | "Actual Rows": 60350, 218 | "Actual Loops": 1, 219 | "Output": ["ol.orderlineid", "ol.orderid", "ol.prod_id", "ol.quantity", "ol.orderdate"], 220 | "Shared Hit Blocks": 2, 221 | "Shared Read Blocks": 383, 222 | "Shared Dirtied Blocks": 0, 223 | "Shared Written Blocks": 0, 224 | "Local Hit Blocks": 0, 225 | "Local Read Blocks": 0, 226 | "Local Dirtied Blocks": 0, 227 | "Local Written Blocks": 0, 228 | "Temp Read Blocks": 0, 229 | "Temp Written Blocks": 0, 230 | "I/O Read Time": 0.000, 231 | "I/O Write Time": 0.000 232 | }, 233 | { 234 | "Node Type": "Hash", 235 | "Parent Relationship": "Inner", 236 | "Startup Cost": 339.86, 237 | "Total Cost": 339.86, 238 | "Plan Rows": 10000, 239 | "Plan Width": 122, 240 | "Actual Startup Time": 12.446, 241 | "Actual Total Time": 12.446, 242 | "Actual Rows": 10000, 243 | "Actual Loops": 1, 244 | "Output": ["p.prod_id", "cat.categoryname"], 245 | "Hash Buckets": 1024, 246 | "Hash Batches": 1, 247 | "Original Hash Batches": 1, 248 | "Peak Memory Usage": 425, 249 | "Shared Hit Blocks": 2, 250 | "Shared Read Blocks": 100, 251 | "Shared Dirtied Blocks": 0, 252 | "Shared Written Blocks": 0, 253 | "Local Hit Blocks": 0, 254 | "Local Read Blocks": 0, 255 | "Local Dirtied Blocks": 0, 256 | "Local Written Blocks": 0, 257 | "Temp Read Blocks": 0, 258 | "Temp Written Blocks": 0, 259 | "I/O Read Time": 0.000, 260 | "I/O Write Time": 0.000, 261 | "Plans": [ 262 | { 263 | "Node Type": "Hash Join", 264 | "Parent Relationship": "Outer", 265 | "Join Type": "Inner", 266 | "Startup Cost": 1.36, 267 | "Total Cost": 339.86, 268 | "Plan Rows": 10000, 269 | "Plan Width": 122, 270 | "Actual Startup Time": 0.283, 271 | "Actual Total Time": 9.015, 272 | "Actual Rows": 10000, 273 | "Actual Loops": 1, 274 | "Output": ["p.prod_id", "cat.categoryname"], 275 | "Hash Cond": "(p.category = cat.category)", 276 | "Shared Hit Blocks": 2, 277 | "Shared Read Blocks": 100, 278 | "Shared Dirtied Blocks": 0, 279 | "Shared Written Blocks": 0, 280 | "Local Hit Blocks": 0, 281 | "Local Read Blocks": 0, 282 | "Local Dirtied Blocks": 0, 283 | "Local Written Blocks": 0, 284 | "Temp Read Blocks": 0, 285 | "Temp Written Blocks": 0, 286 | "I/O Read Time": 0.000, 287 | "I/O Write Time": 0.000, 288 | "Plans": [ 289 | { 290 | "Node Type": "Seq Scan", 291 | "Parent Relationship": "Outer", 292 | "Relation Name": "products", 293 | "Schema": "public", 294 | "Alias": "p", 295 | "Startup Cost": 0.00, 296 | "Total Cost": 201.00, 297 | "Plan Rows": 10000, 298 | "Plan Width": 8, 299 | "Actual Startup Time": 0.003, 300 | "Actual Total Time": 4.330, 301 | "Actual Rows": 10000, 302 | "Actual Loops": 1, 303 | "Output": ["p.prod_id", "p.category", "p.title", "p.actor", "p.price", "p.special", "p.common_prod_id"], 304 | "Shared Hit Blocks": 2, 305 | "Shared Read Blocks": 99, 306 | "Shared Dirtied Blocks": 0, 307 | "Shared Written Blocks": 0, 308 | "Local Hit Blocks": 0, 309 | "Local Read Blocks": 0, 310 | "Local Dirtied Blocks": 0, 311 | "Local Written Blocks": 0, 312 | "Temp Read Blocks": 0, 313 | "Temp Written Blocks": 0, 314 | "I/O Read Time": 0.000, 315 | "I/O Write Time": 0.000 316 | }, 317 | { 318 | "Node Type": "Hash", 319 | "Parent Relationship": "Inner", 320 | "Startup Cost": 1.16, 321 | "Total Cost": 1.16, 322 | "Plan Rows": 16, 323 | "Plan Width": 122, 324 | "Actual Startup Time": 0.265, 325 | "Actual Total Time": 0.265, 326 | "Actual Rows": 16, 327 | "Actual Loops": 1, 328 | "Output": ["cat.categoryname", "cat.category"], 329 | "Hash Buckets": 1024, 330 | "Hash Batches": 1, 331 | "Original Hash Batches": 1, 332 | "Peak Memory Usage": 1, 333 | "Shared Hit Blocks": 0, 334 | "Shared Read Blocks": 1, 335 | "Shared Dirtied Blocks": 0, 336 | "Shared Written Blocks": 0, 337 | "Local Hit Blocks": 0, 338 | "Local Read Blocks": 0, 339 | "Local Dirtied Blocks": 0, 340 | "Local Written Blocks": 0, 341 | "Temp Read Blocks": 0, 342 | "Temp Written Blocks": 0, 343 | "I/O Read Time": 0.000, 344 | "I/O Write Time": 0.000, 345 | "Plans": [ 346 | { 347 | "Node Type": "Seq Scan", 348 | "Parent Relationship": "Outer", 349 | "Relation Name": "categories", 350 | "Schema": "public", 351 | "Alias": "cat", 352 | "Startup Cost": 0.00, 353 | "Total Cost": 1.16, 354 | "Plan Rows": 16, 355 | "Plan Width": 122, 356 | "Actual Startup Time": 0.250, 357 | "Actual Total Time": 0.252, 358 | "Actual Rows": 16, 359 | "Actual Loops": 1, 360 | "Output": ["cat.categoryname", "cat.category"], 361 | "Shared Hit Blocks": 0, 362 | "Shared Read Blocks": 1, 363 | "Shared Dirtied Blocks": 0, 364 | "Shared Written Blocks": 0, 365 | "Local Hit Blocks": 0, 366 | "Local Read Blocks": 0, 367 | "Local Dirtied Blocks": 0, 368 | "Local Written Blocks": 0, 369 | "Temp Read Blocks": 0, 370 | "Temp Written Blocks": 0, 371 | "I/O Read Time": 0.000, 372 | "I/O Write Time": 0.000 373 | } 374 | ] 375 | } 376 | ] 377 | } 378 | ] 379 | } 380 | ] 381 | }, 382 | { 383 | "Node Type": "Hash", 384 | "Parent Relationship": "Inner", 385 | "Startup Cost": 220.00, 386 | "Total Cost": 220.00, 387 | "Plan Rows": 12000, 388 | "Plan Width": 16, 389 | "Actual Startup Time": 10.159, 390 | "Actual Total Time": 10.159, 391 | "Actual Rows": 12000, 392 | "Actual Loops": 1, 393 | "Output": ["o.netamount", "o.totalamount", "o.orderid"], 394 | "Hash Buckets": 2048, 395 | "Hash Batches": 1, 396 | "Original Hash Batches": 1, 397 | "Peak Memory Usage": 609, 398 | "Shared Hit Blocks": 2, 399 | "Shared Read Blocks": 98, 400 | "Shared Dirtied Blocks": 0, 401 | "Shared Written Blocks": 0, 402 | "Local Hit Blocks": 0, 403 | "Local Read Blocks": 0, 404 | "Local Dirtied Blocks": 0, 405 | "Local Written Blocks": 0, 406 | "Temp Read Blocks": 0, 407 | "Temp Written Blocks": 0, 408 | "I/O Read Time": 0.000, 409 | "I/O Write Time": 0.000, 410 | "Plans": [ 411 | { 412 | "Node Type": "Seq Scan", 413 | "Parent Relationship": "Outer", 414 | "Relation Name": "orders", 415 | "Schema": "public", 416 | "Alias": "o", 417 | "Startup Cost": 0.00, 418 | "Total Cost": 220.00, 419 | "Plan Rows": 12000, 420 | "Plan Width": 16, 421 | "Actual Startup Time": 0.008, 422 | "Actual Total Time": 5.548, 423 | "Actual Rows": 12000, 424 | "Actual Loops": 1, 425 | "Output": ["o.netamount", "o.totalamount", "o.orderid"], 426 | "Shared Hit Blocks": 2, 427 | "Shared Read Blocks": 98, 428 | "Shared Dirtied Blocks": 0, 429 | "Shared Written Blocks": 0, 430 | "Local Hit Blocks": 0, 431 | "Local Read Blocks": 0, 432 | "Local Dirtied Blocks": 0, 433 | "Local Written Blocks": 0, 434 | "Temp Read Blocks": 0, 435 | "Temp Written Blocks": 0, 436 | "I/O Read Time": 0.000, 437 | "I/O Write Time": 0.000 438 | } 439 | ] 440 | } 441 | ] 442 | }, 443 | { 444 | "Node Type": "Hash", 445 | "Parent Relationship": "Inner", 446 | "Startup Cost": 3377.25, 447 | "Total Cost": 3377.25, 448 | "Plan Rows": 60350, 449 | "Plan Width": 7, 450 | "Actual Startup Time": 95.610, 451 | "Actual Total Time": 95.610, 452 | "Actual Rows": 60350, 453 | "Actual Loops": 1, 454 | "Output": ["c.state", "ch.orderid"], 455 | "Hash Buckets": 8192, 456 | "Hash Batches": 1, 457 | "Original Hash Batches": 1, 458 | "Peak Memory Usage": 2239, 459 | "Shared Hit Blocks": 4, 460 | "Shared Read Blocks": 811, 461 | "Shared Dirtied Blocks": 0, 462 | "Shared Written Blocks": 0, 463 | "Local Hit Blocks": 0, 464 | "Local Read Blocks": 0, 465 | "Local Dirtied Blocks": 0, 466 | "Local Written Blocks": 0, 467 | "Temp Read Blocks": 0, 468 | "Temp Written Blocks": 0, 469 | "I/O Read Time": 0.000, 470 | "I/O Write Time": 0.000, 471 | "Plans": [ 472 | { 473 | "Node Type": "Hash Join", 474 | "Parent Relationship": "Outer", 475 | "Join Type": "Inner", 476 | "Startup Cost": 938.00, 477 | "Total Cost": 3377.25, 478 | "Plan Rows": 60350, 479 | "Plan Width": 7, 480 | "Actual Startup Time": 24.115, 481 | "Actual Total Time": 74.639, 482 | "Actual Rows": 60350, 483 | "Actual Loops": 1, 484 | "Output": ["c.state", "ch.orderid"], 485 | "Hash Cond": "(ch.customerid = c.customerid)", 486 | "Shared Hit Blocks": 4, 487 | "Shared Read Blocks": 811, 488 | "Shared Dirtied Blocks": 0, 489 | "Shared Written Blocks": 0, 490 | "Local Hit Blocks": 0, 491 | "Local Read Blocks": 0, 492 | "Local Dirtied Blocks": 0, 493 | "Local Written Blocks": 0, 494 | "Temp Read Blocks": 0, 495 | "Temp Written Blocks": 0, 496 | "I/O Read Time": 0.000, 497 | "I/O Write Time": 0.000, 498 | "Plans": [ 499 | { 500 | "Node Type": "Seq Scan", 501 | "Parent Relationship": "Outer", 502 | "Relation Name": "cust_hist", 503 | "Schema": "public", 504 | "Alias": "ch", 505 | "Startup Cost": 0.00, 506 | "Total Cost": 930.50, 507 | "Plan Rows": 60350, 508 | "Plan Width": 8, 509 | "Actual Startup Time": 0.294, 510 | "Actual Total Time": 11.812, 511 | "Actual Rows": 60350, 512 | "Actual Loops": 1, 513 | "Output": ["ch.customerid", "ch.orderid", "ch.prod_id"], 514 | "Shared Hit Blocks": 2, 515 | "Shared Read Blocks": 325, 516 | "Shared Dirtied Blocks": 0, 517 | "Shared Written Blocks": 0, 518 | "Local Hit Blocks": 0, 519 | "Local Read Blocks": 0, 520 | "Local Dirtied Blocks": 0, 521 | "Local Written Blocks": 0, 522 | "Temp Read Blocks": 0, 523 | "Temp Written Blocks": 0, 524 | "I/O Read Time": 0.000, 525 | "I/O Write Time": 0.000 526 | }, 527 | { 528 | "Node Type": "Hash", 529 | "Parent Relationship": "Inner", 530 | "Startup Cost": 688.00, 531 | "Total Cost": 688.00, 532 | "Plan Rows": 20000, 533 | "Plan Width": 7, 534 | "Actual Startup Time": 23.786, 535 | "Actual Total Time": 23.786, 536 | "Actual Rows": 20000, 537 | "Actual Loops": 1, 538 | "Output": ["c.state", "c.customerid"], 539 | "Hash Buckets": 2048, 540 | "Hash Batches": 1, 541 | "Original Hash Batches": 1, 542 | "Peak Memory Usage": 743, 543 | "Shared Hit Blocks": 2, 544 | "Shared Read Blocks": 486, 545 | "Shared Dirtied Blocks": 0, 546 | "Shared Written Blocks": 0, 547 | "Local Hit Blocks": 0, 548 | "Local Read Blocks": 0, 549 | "Local Dirtied Blocks": 0, 550 | "Local Written Blocks": 0, 551 | "Temp Read Blocks": 0, 552 | "Temp Written Blocks": 0, 553 | "I/O Read Time": 0.000, 554 | "I/O Write Time": 0.000, 555 | "Plans": [ 556 | { 557 | "Node Type": "Seq Scan", 558 | "Parent Relationship": "Outer", 559 | "Relation Name": "customers", 560 | "Schema": "public", 561 | "Alias": "c", 562 | "Startup Cost": 0.00, 563 | "Total Cost": 688.00, 564 | "Plan Rows": 20000, 565 | "Plan Width": 7, 566 | "Actual Startup Time": 0.005, 567 | "Actual Total Time": 16.771, 568 | "Actual Rows": 20000, 569 | "Actual Loops": 1, 570 | "Output": ["c.state", "c.customerid"], 571 | "Shared Hit Blocks": 2, 572 | "Shared Read Blocks": 486, 573 | "Shared Dirtied Blocks": 0, 574 | "Shared Written Blocks": 0, 575 | "Local Hit Blocks": 0, 576 | "Local Read Blocks": 0, 577 | "Local Dirtied Blocks": 0, 578 | "Local Written Blocks": 0, 579 | "Temp Read Blocks": 0, 580 | "Temp Written Blocks": 0, 581 | "I/O Read Time": 0.000, 582 | "I/O Write Time": 0.000 583 | } 584 | ] 585 | } 586 | ] 587 | } 588 | ] 589 | } 590 | ] 591 | } 592 | ] 593 | } 594 | ] 595 | } 596 | ] 597 | }, 598 | "Planning Time": 26.171, 599 | "Triggers": [ 600 | ], 601 | "Execution Time": 726.800 602 | } 603 | ]`; 604 | 605 | export var SAMPLE_QUERY = `SELECT c.state, 606 | cat.categoryname, 607 | sum(o.netamount), 608 | sum(o.totalamount) 609 | FROM customers c 610 | INNER JOIN cust_hist ch ON c.customerid = ch.customerid 611 | INNER JOIN orders o ON ch.orderid = o.orderid 612 | INNER JOIN orderlines ol ON ol.orderid = o.orderid 613 | INNER JOIN products p ON ol.prod_id = p.prod_id 614 | INNER JOIN categories cat ON p.category = cat.category 615 | GROUP BY c.state, cat.categoryname 616 | ORDER BY c.state, sum(o.totalamount) DESC 617 | LIMIT 10 OFFSET 1`; 618 | -------------------------------------------------------------------------------- /app/components/plan-node/plan-node.html: -------------------------------------------------------------------------------- 1 |
5 | 6 |
7 |

{{getNodeName()}}

8 | 9 | {{node[_planService.ACTUAL_DURATION_PROP] | duration}}{{node[_planService.ACTUAL_DURATION_PROP] | durationUnit}} 10 | | {{executionTimePercent}} 11 | % 12 | 13 | 14 |
15 | 16 | 20 | 21 |
22 |
23 | on 24 | {{node[_planService.SCHEMA_PROP]}}.{{node[_planService.RELATION_NAME_PROP]}} 25 | ({{node[_planService.ALIAS_PROP]}}) 26 |
27 | 28 |
29 | by {{node[_planService.GROUP_KEY_PROP]}}
30 |
31 | by {{node[_planService.SORT_KEY_PROP]}}
32 |
{{node[_planService.JOIN_TYPE_PROP]}} 33 | join
34 |
35 | using {{node[_planService.INDEX_NAME_PROP]}}
36 |
37 | on {{node[_planService.HASH_CONDITION_PROP]}}
38 |
39 | CTE {{node[_planService.CTE_NAME_PROP]}} 40 |
41 |
42 | 43 |
44 | {{getTagName(tag)}} 45 |
46 | 47 |
48 |
49 | 50 |
51 | 52 | {{viewOptions.highlightType}}: {{highlightValue | number:'.0-2'}} 53 | 54 |
55 | 56 |
57 | over estimated rows 58 | under estimated rows 59 | by {{plannerRowEstimateValue | number}}x 60 |
61 | 62 |
63 |
64 | {{node[_planService.NODE_TYPE_PROP]}} Node 65 |
66 | 67 | 68 | 69 | 70 | 71 | 72 |
{{prop.key}}{{prop.value}}
73 |
*Pev calculated value
74 |
75 | 76 |
77 | 80 |

query

81 |
82 |
83 |
84 | 85 | 90 | -------------------------------------------------------------------------------- /app/components/plan-node/plan-node.ts: -------------------------------------------------------------------------------- 1 | import {IPlan} from '../../interfaces/iplan'; 2 | import {Component, OnInit} from 'angular2/core'; 3 | import {HighlightType, EstimateDirection, ViewMode} from '../../enums'; 4 | import {DurationPipe, DurationUnitPipe} from '../../pipes'; 5 | 6 | import {PlanService} from '../../services/plan-service'; 7 | import {SyntaxHighlightService} from '../../services/syntax-highlight-service'; 8 | import {HelpService} from '../../services/help-service'; 9 | import {ColorService} from '../../services/color-service'; 10 | 11 | /// 12 | 13 | @Component({ 14 | selector: 'plan-node', 15 | inputs: ['plan', 'node', 'viewOptions'], 16 | templateUrl: './components/plan-node/plan-node.html', 17 | directives: [PlanNode], 18 | providers: [PlanService, SyntaxHighlightService, HelpService, ColorService], 19 | pipes: [DurationPipe, DurationUnitPipe] 20 | }) 21 | 22 | export class PlanNode { 23 | // consts 24 | NORMAL_WIDTH: number = 220; 25 | COMPACT_WIDTH: number = 140; 26 | DOT_WIDTH: number = 30; 27 | EXPANDED_WIDTH: number = 400; 28 | 29 | MIN_ESTIMATE_MISS: number = 100; 30 | COSTLY_TAG: string = 'costliest'; 31 | SLOW_TAG: string = 'slowest'; 32 | LARGE_TAG: string = 'largest'; 33 | ESTIMATE_TAG: string = 'bad estimate'; 34 | 35 | // inputs 36 | plan: IPlan; 37 | node: any; 38 | viewOptions: any; 39 | 40 | // UI flags 41 | showDetails: boolean; 42 | 43 | // calculated properties 44 | executionTimePercent: number; 45 | backgroundColor: string; 46 | highlightValue: number; 47 | barContainerWidth: number; 48 | barWidth: number; 49 | props: Array; 50 | tags: Array; 51 | plannerRowEstimateValue: number; 52 | plannerRowEstimateDirection: EstimateDirection; 53 | 54 | // required for custom change detection 55 | currentHighlightType: string; 56 | currentCompactView: boolean; 57 | currentExpandedView: boolean; 58 | 59 | // expose enum to view 60 | estimateDirections = EstimateDirection; 61 | highlightTypes = HighlightType; 62 | viewModes = ViewMode; 63 | 64 | constructor(private _planService: PlanService, 65 | private _syntaxHighlightService: SyntaxHighlightService, 66 | private _helpService: HelpService, 67 | private _colorService: ColorService) { } 68 | 69 | ngOnInit() { 70 | this.currentHighlightType = this.viewOptions.highlightType; 71 | this.calculateBar(); 72 | this.calculateProps(); 73 | this.calculateDuration(); 74 | this.calculateTags(); 75 | 76 | this.plannerRowEstimateDirection = this.node[this._planService.PLANNER_ESIMATE_DIRECTION]; 77 | this.plannerRowEstimateValue = _.round(this.node[this._planService.PLANNER_ESTIMATE_FACTOR]); 78 | } 79 | 80 | ngDoCheck() { 81 | if (this.currentHighlightType !== this.viewOptions.highlightType) { 82 | this.currentHighlightType = this.viewOptions.highlightType; 83 | this.calculateBar(); 84 | } 85 | 86 | if (this.currentCompactView !== this.viewOptions.showCompactView) { 87 | this.currentCompactView = this.viewOptions.showCompactView; 88 | this.calculateBar(); 89 | } 90 | 91 | if (this.currentExpandedView !== this.showDetails) { 92 | this.currentExpandedView = this.showDetails; 93 | this.calculateBar(); 94 | } 95 | } 96 | 97 | getFormattedQuery() { 98 | var keyItems: Array = []; 99 | 100 | // relation name will be highlighted for SCAN nodes 101 | var relationName: string = this.node[this._planService.RELATION_NAME_PROP]; 102 | if (relationName) { 103 | keyItems.push(this.node[this._planService.SCHEMA_PROP] + '.' + relationName); 104 | keyItems.push(' ' + relationName); 105 | keyItems.push(' ' + this.node[this._planService.ALIAS_PROP] + ' '); 106 | } 107 | 108 | // group key will be highlighted for AGGREGATE nodes 109 | var groupKey: Array = this.node[this._planService.GROUP_KEY_PROP]; 110 | if (groupKey) { 111 | keyItems.push('GROUP BY ' + groupKey.join(',')); 112 | } 113 | 114 | // hash condition will be highlighted for HASH JOIN nodes 115 | var hashCondition: string = this.node[this._planService.HASH_CONDITION_PROP]; 116 | if (hashCondition) { 117 | keyItems.push(hashCondition.replace('(', '').replace(')', '')); 118 | } 119 | 120 | if (this.node[this._planService.NODE_TYPE_PROP].toUpperCase() === 'LIMIT') { 121 | keyItems.push('LIMIT'); 122 | } 123 | return this._syntaxHighlightService.highlight(this.plan.query, keyItems); 124 | } 125 | 126 | calculateBar() { 127 | switch (this.viewOptions.viewMode) { 128 | case ViewMode.DOT: 129 | this.barContainerWidth = this.DOT_WIDTH; 130 | break; 131 | case ViewMode.COMPACT: 132 | this.barContainerWidth = this.COMPACT_WIDTH; 133 | break; 134 | default: 135 | this.barContainerWidth = this.NORMAL_WIDTH; 136 | break; 137 | } 138 | 139 | // expanded view width trumps others 140 | if (this.currentExpandedView) { 141 | this.barContainerWidth = this.EXPANDED_WIDTH; 142 | } 143 | 144 | switch (this.currentHighlightType) { 145 | case HighlightType.DURATION: 146 | this.highlightValue = (this.node[this._planService.ACTUAL_DURATION_PROP]); 147 | this.barWidth = Math.round((this.highlightValue / this.plan.planStats.maxDuration) * this.barContainerWidth); 148 | break; 149 | case HighlightType.ROWS: 150 | this.highlightValue = (this.node[this._planService.ACTUAL_ROWS_PROP]); 151 | this.barWidth = Math.round((this.highlightValue / this.plan.planStats.maxRows) * this.barContainerWidth); 152 | break; 153 | case HighlightType.COST: 154 | this.highlightValue = (this.node[this._planService.ACTUAL_COST_PROP]); 155 | this.barWidth = Math.round((this.highlightValue / this.plan.planStats.maxCost) * this.barContainerWidth); 156 | break; 157 | } 158 | 159 | if (this.barWidth < 1) { 160 | this.barWidth = 1; 161 | } 162 | 163 | this.backgroundColor = this._colorService.numberToColorHsl(1 - this.barWidth / this.barContainerWidth); 164 | } 165 | 166 | calculateDuration() { 167 | this.executionTimePercent = (_.round((this.node[this._planService.ACTUAL_DURATION_PROP] / this.plan.planStats.executionTime) * 100)); 168 | } 169 | 170 | // create an array of node propeties so that they can be displayed in the view 171 | calculateProps() { 172 | this.props = _.chain(this.node) 173 | .omit(this._planService.PLANS_PROP) 174 | .map((value, key) => { 175 | return { key: key, value: value }; 176 | }) 177 | .value(); 178 | } 179 | 180 | calculateTags() { 181 | this.tags = []; 182 | if (this.node[this._planService.SLOWEST_NODE_PROP]) { 183 | this.tags.push(this.SLOW_TAG); 184 | } 185 | if (this.node[this._planService.COSTLIEST_NODE_PROP]) { 186 | this.tags.push(this.COSTLY_TAG); 187 | } 188 | if (this.node[this._planService.LARGEST_NODE_PROP]) { 189 | this.tags.push(this.LARGE_TAG); 190 | } 191 | if (this.node[this._planService.PLANNER_ESTIMATE_FACTOR] >= this.MIN_ESTIMATE_MISS) { 192 | this.tags.push(this.ESTIMATE_TAG); 193 | } 194 | } 195 | 196 | getNodeTypeDescription() { 197 | return this._helpService.getNodeTypeDescription(this.node[this._planService.NODE_TYPE_PROP]); 198 | } 199 | 200 | getNodeName() { 201 | if (this.viewOptions.viewMode === ViewMode.DOT && !this.showDetails) { 202 | return this.node[this._planService.NODE_TYPE_PROP].replace(/[^A-Z]/g, '').toUpperCase(); 203 | } 204 | 205 | return (this.node[this._planService.NODE_TYPE_PROP]).toUpperCase(); 206 | } 207 | 208 | getTagName(tagName: String) { 209 | if (this.viewOptions.viewMode === ViewMode.DOT && !this.showDetails) { 210 | return tagName.charAt(0); 211 | } 212 | return tagName; 213 | } 214 | 215 | shouldShowPlannerEstimate() { 216 | if (this.viewOptions.showPlannerEstimate && this.showDetails) { 217 | return true; 218 | } 219 | 220 | if (this.viewOptions.viewMode === ViewMode.DOT) { 221 | return false; 222 | } 223 | 224 | return this.viewOptions.showPlannerEstimate; 225 | } 226 | 227 | shouldShowNodeBarLabel() { 228 | if (this.showDetails) { 229 | return true; 230 | } 231 | 232 | if (this.viewOptions.viewMode === ViewMode.DOT) { 233 | return false; 234 | } 235 | 236 | return true; 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /app/components/plan-view/plan-view.html: -------------------------------------------------------------------------------- 1 | 40 | 41 |
42 |

{{plan.name}} 43 | 44 | 47 |

48 | 49 |
50 |
51 | {{plan.planStats.executionTime | duration}} 52 | execution time ({{plan.planStats.executionTime | durationUnit}}) 53 |
54 |
55 | {{plan.planStats.planningTime | number:'.0-2'}} 56 | planning time (ms) 57 |
58 |
59 | {{plan.planStats.maxDuration | duration}} 60 | slowest node ({{plan.planStats.maxDuration | durationUnit}}) 61 |
62 |
63 | {{plan.planStats.maxRows | number:'.0-2'}} 64 | largest node (rows) 65 |
66 |
67 | {{plan.planStats.maxCost | number:'.0-2'}} 68 | costliest node 69 |
70 |
71 |
72 | 73 |
74 |
    75 |
  • 76 | 77 |
  • 78 |
79 |
80 |
81 | -------------------------------------------------------------------------------- /app/components/plan-view/plan-view.ts: -------------------------------------------------------------------------------- 1 | import {Component, OnInit} from 'angular2/core'; 2 | import {ROUTER_DIRECTIVES, RouteParams} from 'angular2/router'; 3 | 4 | import {IPlan} from '../../interfaces/iplan'; 5 | import {HighlightType, ViewMode} from '../../enums'; 6 | import {PlanNode} from '../plan-node/plan-node'; 7 | import {PlanService} from '../../services/plan-service'; 8 | import {SyntaxHighlightService} from '../../services/syntax-highlight-service'; 9 | import {DurationPipe, DurationUnitPipe} from '../../pipes'; 10 | 11 | @Component({ 12 | selector: 'plan-view', 13 | templateUrl: './components/plan-view/plan-view.html', 14 | directives: [ROUTER_DIRECTIVES, PlanNode], 15 | providers: [PlanService, SyntaxHighlightService], 16 | pipes: [DurationPipe, DurationUnitPipe] 17 | }) 18 | export class PlanView { 19 | id: string; 20 | plan: IPlan; 21 | rootContainer: any; 22 | hideMenu: boolean = true; 23 | 24 | viewOptions: any = { 25 | showPlanStats: true, 26 | showHighlightBar: true, 27 | showPlannerEstimate: false, 28 | showTags: true, 29 | highlightType: HighlightType.NONE, 30 | viewMode: ViewMode.FULL 31 | }; 32 | 33 | showPlannerEstimate: boolean = true; 34 | showMenu: boolean = false; 35 | 36 | highlightTypes = HighlightType; // exposing the enum to the view 37 | viewModes = ViewMode; 38 | 39 | constructor(private _planService: PlanService, routeParams: RouteParams) { 40 | this.id = routeParams.get('id'); 41 | } 42 | 43 | getPlan() { 44 | if (!this.id) { 45 | return; 46 | } 47 | 48 | this.plan = this._planService.getPlan(this.id); 49 | this.rootContainer = this.plan.content; 50 | this.plan.planStats = { 51 | executionTime: this.rootContainer['Execution Time'] || this.rootContainer['Total Runtime'], 52 | planningTime: this.rootContainer['Planning Time'] || 0, 53 | maxRows: this.rootContainer[this._planService.MAXIMUM_ROWS_PROP] || 0, 54 | maxCost: this.rootContainer[this._planService.MAXIMUM_COSTS_PROP] || 0, 55 | maxDuration: this.rootContainer[this._planService.MAXIMUM_DURATION_PROP] || 0 56 | }; 57 | } 58 | 59 | ngOnInit() { 60 | this.getPlan(); 61 | } 62 | 63 | toggleHighlight(type: HighlightType) { 64 | this.viewOptions.highlightType = type; 65 | } 66 | 67 | analyzePlan() { 68 | this._planService.analyzePlan(this.plan); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /app/enums.ts: -------------------------------------------------------------------------------- 1 | export class HighlightType { 2 | static NONE: string = 'none'; 3 | static DURATION: string = 'duration'; 4 | static ROWS: string = 'rows'; 5 | static COST: string = 'cost'; 6 | } 7 | 8 | export enum EstimateDirection { 9 | over, 10 | under 11 | } 12 | 13 | export class ViewMode { 14 | static FULL: string = 'full'; 15 | static COMPACT: string = 'compact'; 16 | static DOT: string = 'dot'; 17 | } 18 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <%= APP_TITLE %> 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Loading... 15 | 16 | 17 | 18 | 21 | 22 | 23 | 24 | 25 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/interfaces/iplan.ts: -------------------------------------------------------------------------------- 1 | export interface IPlan { 2 | id: string; 3 | name: string; 4 | content: any; 5 | query: string; 6 | createdOn: Date; 7 | planStats: any; 8 | formattedQuery: string; 9 | } 10 | -------------------------------------------------------------------------------- /app/pipes.ts: -------------------------------------------------------------------------------- 1 | import {Pipe} from 'angular2/core'; 2 | /// 3 | 4 | @Pipe({ name: 'momentDate' }) 5 | export class MomentDatePipe { 6 | transform(value: string, args: string[]): any { 7 | return moment(value).format('LLL'); 8 | } 9 | } 10 | 11 | @Pipe({ name: 'duration' }) 12 | export class DurationPipe { 13 | transform(value: number): string { 14 | var duration: string = ''; 15 | 16 | if (value < 1) { 17 | duration = '<1'; 18 | } else if (value > 1 && value < 1000) { 19 | duration = _.round(value, 2).toString(); 20 | } else if (value >= 1000 && value < 60000) { 21 | duration = _.round(value / 1000, 2).toString(); 22 | } else if (value >= 60000) { 23 | duration = _.round(value / 60000, 2).toString(); 24 | } 25 | return duration; 26 | } 27 | } 28 | 29 | @Pipe({ name: 'durationUnit' }) 30 | export class DurationUnitPipe { 31 | transform(value: number) { 32 | var unit: string = ''; 33 | 34 | if (value < 1) { 35 | unit = 'ms'; 36 | } else if (value > 1 && value < 1000) { 37 | unit = 'ms'; 38 | } else if (value >= 1000 && value < 60000) { 39 | unit = 's'; 40 | } else if (value >= 60000) { 41 | unit = 'min'; 42 | } 43 | return unit; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /app/sample-plans/plan1.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "Plan": { 4 | "Node Type": "Limit", 5 | "Startup Cost": 60970.08, 6 | "Total Cost": 60970.11, 7 | "Plan Rows": 10, 8 | "Plan Width": 468, 9 | "Actual Startup Time": 1941.751, 10 | "Actual Total Time": 1941.780, 11 | "Actual Rows": 10, 12 | "Actual Loops": 1, 13 | "Output": ["a.id", "(COALESCE(a.display_name, a.alert_name))", "a.external_alert_id", "a.venue_code", "altt.code", "altt.name", "altst.name", "a.stop_type", "a.deploy_mode", "a.estimated_cost", "(sum(s.total_alerts))", "(sum(s.total_followed))", "(sum(s.total_overridden))", "(sum(s.total_ignored))", "(sum(s.total_not_seen))", "(sum(s.total_unknown))", "(percenttotal((sum(s.total_followed)), (sum(s.total_alerts))))", "(percenttotal((sum(s.total_overridden)), (sum(s.total_alerts))))", "(percenttotal((sum(s.total_ignored)), (sum(s.total_alerts))))", "(percenttotal((sum(s.total_not_seen)), (sum(s.total_alerts))))", "(percenttotal((sum(s.total_unknown)), (sum(s.total_alerts))))", "(((sum(s.total_alerts)) * a.estimated_cost))"], 14 | "Shared Hit Blocks": 260587, 15 | "Shared Read Blocks": 0, 16 | "Shared Dirtied Blocks": 0, 17 | "Shared Written Blocks": 0, 18 | "Local Hit Blocks": 0, 19 | "Local Read Blocks": 0, 20 | "Local Dirtied Blocks": 0, 21 | "Local Written Blocks": 0, 22 | "Temp Read Blocks": 0, 23 | "Temp Written Blocks": 0, 24 | "I/O Read Time": 0.000, 25 | "I/O Write Time": 0.000, 26 | "Plans": [ 27 | { 28 | "Node Type": "Sort", 29 | "Parent Relationship": "Outer", 30 | "Startup Cost": 60970.08, 31 | "Total Cost": 60977.40, 32 | "Plan Rows": 2927, 33 | "Plan Width": 468, 34 | "Actual Startup Time": 1941.747, 35 | "Actual Total Time": 1941.757, 36 | "Actual Rows": 10, 37 | "Actual Loops": 1, 38 | "Output": ["a.id", "(COALESCE(a.display_name, a.alert_name))", "a.external_alert_id", "a.venue_code", "altt.code", "altt.name", "altst.name", "a.stop_type", "a.deploy_mode", "a.estimated_cost", "(sum(s.total_alerts))", "(sum(s.total_followed))", "(sum(s.total_overridden))", "(sum(s.total_ignored))", "(sum(s.total_not_seen))", "(sum(s.total_unknown))", "(percenttotal((sum(s.total_followed)), (sum(s.total_alerts))))", "(percenttotal((sum(s.total_overridden)), (sum(s.total_alerts))))", "(percenttotal((sum(s.total_ignored)), (sum(s.total_alerts))))", "(percenttotal((sum(s.total_not_seen)), (sum(s.total_alerts))))", "(percenttotal((sum(s.total_unknown)), (sum(s.total_alerts))))", "(((sum(s.total_alerts)) * a.estimated_cost))"], 39 | "Sort Key": ["(sum(s.total_alerts))"], 40 | "Sort Method": "top-N heapsort", 41 | "Sort Space Used": 27, 42 | "Sort Space Type": "Memory", 43 | "Shared Hit Blocks": 260587, 44 | "Shared Read Blocks": 0, 45 | "Shared Dirtied Blocks": 0, 46 | "Shared Written Blocks": 0, 47 | "Local Hit Blocks": 0, 48 | "Local Read Blocks": 0, 49 | "Local Dirtied Blocks": 0, 50 | "Local Written Blocks": 0, 51 | "Temp Read Blocks": 0, 52 | "Temp Written Blocks": 0, 53 | "I/O Read Time": 0.000, 54 | "I/O Write Time": 0.000, 55 | "Plans": [ 56 | { 57 | "Node Type": "Nested Loop", 58 | "Parent Relationship": "Outer", 59 | "Join Type": "Left", 60 | "Startup Cost": 37552.36, 61 | "Total Cost": 60906.83, 62 | "Plan Rows": 2927, 63 | "Plan Width": 468, 64 | "Actual Startup Time": 522.250, 65 | "Actual Total Time": 1865.979, 66 | "Actual Rows": 32352, 67 | "Actual Loops": 1, 68 | "Output": ["a.id", "COALESCE(a.display_name, a.alert_name)", "a.external_alert_id", "a.venue_code", "altt.code", "altt.name", "altst.name", "a.stop_type", "a.deploy_mode", "a.estimated_cost", "(sum(s.total_alerts))", "(sum(s.total_followed))", "(sum(s.total_overridden))", "(sum(s.total_ignored))", "(sum(s.total_not_seen))", "(sum(s.total_unknown))", "percenttotal((sum(s.total_followed)), (sum(s.total_alerts)))", "percenttotal((sum(s.total_overridden)), (sum(s.total_alerts)))", "percenttotal((sum(s.total_ignored)), (sum(s.total_alerts)))", "percenttotal((sum(s.total_not_seen)), (sum(s.total_alerts)))", "percenttotal((sum(s.total_unknown)), (sum(s.total_alerts)))", "((sum(s.total_alerts)) * a.estimated_cost)"], 69 | "Shared Hit Blocks": 260587, 70 | "Shared Read Blocks": 0, 71 | "Shared Dirtied Blocks": 0, 72 | "Shared Written Blocks": 0, 73 | "Local Hit Blocks": 0, 74 | "Local Read Blocks": 0, 75 | "Local Dirtied Blocks": 0, 76 | "Local Written Blocks": 0, 77 | "Temp Read Blocks": 0, 78 | "Temp Written Blocks": 0, 79 | "I/O Read Time": 0.000, 80 | "I/O Write Time": 0.000, 81 | "Plans": [ 82 | { 83 | "Node Type": "Nested Loop", 84 | "Parent Relationship": "Outer", 85 | "Join Type": "Left", 86 | "Startup Cost": 37552.22, 87 | "Total Cost": 56750.41, 88 | "Plan Rows": 2927, 89 | "Plan Width": 461, 90 | "Actual Startup Time": 522.141, 91 | "Actual Total Time": 1161.187, 92 | "Actual Rows": 32352, 93 | "Actual Loops": 1, 94 | "Output": ["(sum(s.total_alerts))", "(sum(s.total_followed))", "(sum(s.total_overridden))", "(sum(s.total_ignored))", "(sum(s.total_not_seen))", "(sum(s.total_unknown))", "a.id", "a.display_name", "a.alert_name", "a.external_alert_id", "a.venue_code", "a.stop_type", "a.deploy_mode", "a.estimated_cost", "a.subtype_id", "altt.code", "altt.name"], 95 | "Shared Hit Blocks": 197115, 96 | "Shared Read Blocks": 0, 97 | "Shared Dirtied Blocks": 0, 98 | "Shared Written Blocks": 0, 99 | "Local Hit Blocks": 0, 100 | "Local Read Blocks": 0, 101 | "Local Dirtied Blocks": 0, 102 | "Local Written Blocks": 0, 103 | "Temp Read Blocks": 0, 104 | "Temp Written Blocks": 0, 105 | "I/O Read Time": 0.000, 106 | "I/O Write Time": 0.000, 107 | "Plans": [ 108 | { 109 | "Node Type": "Nested Loop", 110 | "Parent Relationship": "Outer", 111 | "Join Type": "Inner", 112 | "Startup Cost": 37552.07, 113 | "Total Cost": 56230.56, 114 | "Plan Rows": 2927, 115 | "Plan Width": 405, 116 | "Actual Startup Time": 522.131, 117 | "Actual Total Time": 918.895, 118 | "Actual Rows": 32352, 119 | "Actual Loops": 1, 120 | "Output": ["(sum(s.total_alerts))", "(sum(s.total_followed))", "(sum(s.total_overridden))", "(sum(s.total_ignored))", "(sum(s.total_not_seen))", "(sum(s.total_unknown))", "a.id", "a.display_name", "a.alert_name", "a.external_alert_id", "a.venue_code", "a.stop_type", "a.deploy_mode", "a.estimated_cost", "a.type_id", "a.subtype_id"], 121 | "Shared Hit Blocks": 132411, 122 | "Shared Read Blocks": 0, 123 | "Shared Dirtied Blocks": 0, 124 | "Shared Written Blocks": 0, 125 | "Local Hit Blocks": 0, 126 | "Local Read Blocks": 0, 127 | "Local Dirtied Blocks": 0, 128 | "Local Written Blocks": 0, 129 | "Temp Read Blocks": 0, 130 | "Temp Written Blocks": 0, 131 | "I/O Read Time": 0.000, 132 | "I/O Write Time": 0.000, 133 | "Plans": [ 134 | { 135 | "Node Type": "Aggregate", 136 | "Strategy": "Hashed", 137 | "Parent Relationship": "Outer", 138 | "Startup Cost": 37551.64, 139 | "Total Cost": 37624.82, 140 | "Plan Rows": 2927, 141 | "Plan Width": 56, 142 | "Actual Startup Time": 522.107, 143 | "Actual Total Time": 587.536, 144 | "Actual Rows": 32352, 145 | "Actual Loops": 1, 146 | "Output": ["s.alert_id", "sum(s.total_alerts)", "sum(s.total_followed)", "sum(s.total_overridden)", "sum(s.total_ignored)", "sum(s.total_not_seen)", "sum(s.total_unknown)"], 147 | "Group Key": ["s.alert_id"], 148 | "Shared Hit Blocks": 2784, 149 | "Shared Read Blocks": 0, 150 | "Shared Dirtied Blocks": 0, 151 | "Shared Written Blocks": 0, 152 | "Local Hit Blocks": 0, 153 | "Local Read Blocks": 0, 154 | "Local Dirtied Blocks": 0, 155 | "Local Written Blocks": 0, 156 | "Temp Read Blocks": 0, 157 | "Temp Written Blocks": 0, 158 | "I/O Read Time": 0.000, 159 | "I/O Write Time": 0.000, 160 | "Plans": [ 161 | { 162 | "Node Type": "Bitmap Heap Scan", 163 | "Parent Relationship": "Outer", 164 | "Relation Name": "alert_daily_summaries", 165 | "Schema": "analytics", 166 | "Alias": "s", 167 | "Startup Cost": 3265.35, 168 | "Total Cost": 35524.50, 169 | "Plan Rows": 115837, 170 | "Plan Width": 56, 171 | "Actual Startup Time": 10.745, 172 | "Actual Total Time": 165.318, 173 | "Actual Rows": 140451, 174 | "Actual Loops": 1, 175 | "Output": ["s.id", "s.org_id", "s.alert_id", "s.alert_day", "s.total_alerts", "s.total_followed", "s.total_overridden", "s.total_ignored", "s.total_not_seen", "s.total_unknown", "s.override_comments"], 176 | "Recheck Cond": "((s.org_id = 2) AND (s.alert_day >= '2015-10-01 00:00:00'::timestamp without time zone) AND (s.alert_day <= '2015-12-01 00:00:00'::timestamp without time zone))", 177 | "Rows Removed by Index Recheck": 0, 178 | "Exact Heap Blocks": 2243, 179 | "Lossy Heap Blocks": 0, 180 | "Shared Hit Blocks": 2784, 181 | "Shared Read Blocks": 0, 182 | "Shared Dirtied Blocks": 0, 183 | "Shared Written Blocks": 0, 184 | "Local Hit Blocks": 0, 185 | "Local Read Blocks": 0, 186 | "Local Dirtied Blocks": 0, 187 | "Local Written Blocks": 0, 188 | "Temp Read Blocks": 0, 189 | "Temp Written Blocks": 0, 190 | "I/O Read Time": 0.000, 191 | "I/O Write Time": 0.000, 192 | "Plans": [ 193 | { 194 | "Node Type": "Bitmap Index Scan", 195 | "Parent Relationship": "Outer", 196 | "Index Name": "alert_daily_summaries_org_id_alert_day_idx", 197 | "Startup Cost": 0.00, 198 | "Total Cost": 3236.39, 199 | "Plan Rows": 115837, 200 | "Plan Width": 0, 201 | "Actual Startup Time": 10.445, 202 | "Actual Total Time": 10.445, 203 | "Actual Rows": 140451, 204 | "Actual Loops": 1, 205 | "Index Cond": "((s.org_id = 2) AND (s.alert_day >= '2015-10-01 00:00:00'::timestamp without time zone) AND (s.alert_day <= '2015-12-01 00:00:00'::timestamp without time zone))", 206 | "Shared Hit Blocks": 541, 207 | "Shared Read Blocks": 0, 208 | "Shared Dirtied Blocks": 0, 209 | "Shared Written Blocks": 0, 210 | "Local Hit Blocks": 0, 211 | "Local Read Blocks": 0, 212 | "Local Dirtied Blocks": 0, 213 | "Local Written Blocks": 0, 214 | "Temp Read Blocks": 0, 215 | "Temp Written Blocks": 0, 216 | "I/O Read Time": 0.000, 217 | "I/O Write Time": 0.000 218 | } 219 | ] 220 | } 221 | ] 222 | }, 223 | { 224 | "Node Type": "Index Scan", 225 | "Parent Relationship": "Inner", 226 | "Scan Direction": "Forward", 227 | "Index Name": "alerts_pkey1", 228 | "Relation Name": "alerts", 229 | "Schema": "analytics", 230 | "Alias": "a", 231 | "Startup Cost": 0.42, 232 | "Total Cost": 6.34, 233 | "Plan Rows": 1, 234 | "Plan Width": 213, 235 | "Actual Startup Time": 0.004, 236 | "Actual Total Time": 0.005, 237 | "Actual Rows": 1, 238 | "Actual Loops": 32352, 239 | "Output": ["a.id", "a.org_id", "a.external_alert_id", "a.alert_name", "a.display_name", "a.estimated_cost", "a.description", "a.source", "a.venue_code", "a.action_expected", "a.cancel_expected", "a.responsible_provider_type", "a.status", "a.version", "a.created_by", "a.created_date", "a.modified_by", "a.modified_date", "a.tracking_id", "a.record_date", "a.released_date", "a.released", "a.comments", "a.default_lockout_hours", "a.deploy_mode", "a.stop_type", "a.importance_level", "a.type_id", "a.subtype_id"], 240 | "Index Cond": "(a.id = s.alert_id)", 241 | "Rows Removed by Index Recheck": 0, 242 | "Shared Hit Blocks": 129627, 243 | "Shared Read Blocks": 0, 244 | "Shared Dirtied Blocks": 0, 245 | "Shared Written Blocks": 0, 246 | "Local Hit Blocks": 0, 247 | "Local Read Blocks": 0, 248 | "Local Dirtied Blocks": 0, 249 | "Local Written Blocks": 0, 250 | "Temp Read Blocks": 0, 251 | "Temp Written Blocks": 0, 252 | "I/O Read Time": 0.000, 253 | "I/O Write Time": 0.000 254 | } 255 | ] 256 | }, 257 | { 258 | "Node Type": "Index Scan", 259 | "Parent Relationship": "Inner", 260 | "Scan Direction": "Forward", 261 | "Index Name": "alert_types_id_org_id_key", 262 | "Relation Name": "alert_types", 263 | "Schema": "analytics", 264 | "Alias": "altt", 265 | "Startup Cost": 0.15, 266 | "Total Cost": 0.17, 267 | "Plan Rows": 1, 268 | "Plan Width": 72, 269 | "Actual Startup Time": 0.002, 270 | "Actual Total Time": 0.003, 271 | "Actual Rows": 1, 272 | "Actual Loops": 32352, 273 | "Output": ["altt.id", "altt.version", "altt.code", "altt.name", "altt.org_id"], 274 | "Index Cond": "(a.type_id = altt.id)", 275 | "Rows Removed by Index Recheck": 0, 276 | "Shared Hit Blocks": 64704, 277 | "Shared Read Blocks": 0, 278 | "Shared Dirtied Blocks": 0, 279 | "Shared Written Blocks": 0, 280 | "Local Hit Blocks": 0, 281 | "Local Read Blocks": 0, 282 | "Local Dirtied Blocks": 0, 283 | "Local Written Blocks": 0, 284 | "Temp Read Blocks": 0, 285 | "Temp Written Blocks": 0, 286 | "I/O Read Time": 0.000, 287 | "I/O Write Time": 0.000 288 | } 289 | ] 290 | }, 291 | { 292 | "Node Type": "Index Scan", 293 | "Parent Relationship": "Inner", 294 | "Scan Direction": "Forward", 295 | "Index Name": "alert_subtypes_id_org_id_key", 296 | "Relation Name": "alert_subtypes", 297 | "Schema": "analytics", 298 | "Alias": "altst", 299 | "Startup Cost": 0.14, 300 | "Total Cost": 0.16, 301 | "Plan Rows": 1, 302 | "Plan Width": 23, 303 | "Actual Startup Time": 0.002, 304 | "Actual Total Time": 0.003, 305 | "Actual Rows": 1, 306 | "Actual Loops": 32352, 307 | "Output": ["altst.id", "altst.version", "altst.code", "altst.name", "altst.org_id"], 308 | "Index Cond": "(a.subtype_id = altst.id)", 309 | "Rows Removed by Index Recheck": 0, 310 | "Shared Hit Blocks": 63472, 311 | "Shared Read Blocks": 0, 312 | "Shared Dirtied Blocks": 0, 313 | "Shared Written Blocks": 0, 314 | "Local Hit Blocks": 0, 315 | "Local Read Blocks": 0, 316 | "Local Dirtied Blocks": 0, 317 | "Local Written Blocks": 0, 318 | "Temp Read Blocks": 0, 319 | "Temp Written Blocks": 0, 320 | "I/O Read Time": 0.000, 321 | "I/O Write Time": 0.000 322 | } 323 | ] 324 | } 325 | ] 326 | } 327 | ] 328 | }, 329 | "Planning Time": 0.507, 330 | "Triggers": [ 331 | ], 332 | "Execution Time": 1953.684 333 | } 334 | ] 335 | -------------------------------------------------------------------------------- /app/services/color-service.ts: -------------------------------------------------------------------------------- 1 | export class ColorService { 2 | /** 3 | * http://stackoverflow.com/questions/2353211/hsl-to-rgb-color-conversion 4 | * 5 | * Converts an HSL color value to RGB. Conversion formula 6 | * adapted from http://en.wikipedia.org/wiki/HSL_color_space. 7 | * Assumes h, s, and l are contained in the set [0, 1] and 8 | * returns r, g, and b in the set [0, 255]. 9 | * 10 | * @param Number h The hue 11 | * @param Number s The saturation 12 | * @param Number l The lightness 13 | * @return Array The RGB representation 14 | */ 15 | hslToRgb(h, s, l) { 16 | var r, g, b; 17 | 18 | if (s === 0) { 19 | r = g = b = l; // achromatic 20 | } else { 21 | function hue2rgb(p, q, t) { 22 | if (t < 0) t += 1; 23 | if (t > 1) t -= 1; 24 | if (t < 1 / 6) return p + (q - p) * 6 * t; 25 | if (t < 1 / 2) return q; 26 | if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; 27 | return p; 28 | } 29 | 30 | var q = l < 0.5 ? l * (1 + s) : l + s - l * s; 31 | var p = 2 * l - q; 32 | r = hue2rgb(p, q, h + 1 / 3); 33 | g = hue2rgb(p, q, h); 34 | b = hue2rgb(p, q, h - 1 / 3); 35 | } 36 | 37 | return [Math.floor(r * 255), Math.floor(g * 255), Math.floor(b * 255)]; 38 | } 39 | 40 | // convert a number to a color using hsl 41 | numberToColorHsl(i) { 42 | // as the function expects a value between 0 and 1, and red = 0° and green = 120° 43 | // we convert the input to the appropriate hue value 44 | var hue = i * 100 * 1.2 / 360; 45 | // we convert hsl to rgb (saturation 100%, lightness 50%) 46 | var rgb = this.hslToRgb(hue, .9, .4); 47 | // we format to css value and return 48 | return 'rgb(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ')'; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /app/services/help-service.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | export class HelpService { 4 | getNodeTypeDescription(nodeType: string) { 5 | return NODE_DESCRIPTIONS[nodeType.toUpperCase()]; 6 | } 7 | } 8 | 9 | export var NODE_DESCRIPTIONS = { 10 | 'LIMIT':'returns a specified number of rows from a record set.', 11 | 'SORT': 'sorts a record set based on the specified sort key.', 12 | 'NESTED LOOP': `merges two record sets by looping through every record in the first set and 13 | trying to find a match in the second set. All matching records are returned.`, 14 | 'MERGE JOIN': `merges two record sets by first sorting them on a join key.`, 15 | 'HASH': `generates a hash table from the records in the input recordset. Hash is used by 16 | Hash Join.`, 17 | 'HASH JOIN': `joins to record sets by hashing one of them (using a Hash Scan).`, 18 | 'AGGREGATE': `groups records together based on a GROUP BY or aggregate function (like sum()).`, 19 | 'HASHAGGREGATE': `groups records together based on a GROUP BY or aggregate function (like sum()). Hash Aggregate uses 20 | a hash to first organize the records by a key.`, 21 | 'SEQ SCAN': `finds relevant records by sequentially scanning the input record set. When reading from a table, 22 | Seq Scans (unlike Index Scans) perform a single read operation (only the table is read).`, 23 | 'INDEX SCAN': `finds relevant records based on an Index. Index Scans perform 2 read operations: one to 24 | read the index and another to read the actual value from the table.`, 25 | 'INDEX ONLY SCAN': `finds relevant records based on an Index. Index Only Scans perform a single read operation 26 | from the index and do not read from the corresponding table.`, 27 | 'BITMAP HEAP SCAN': 'searches through the pages returned by the Bitmap Index Scan for relevant rows.', 28 | 'BITMAP INDEX SCAN': `uses a Bitmap Index (index which uses 1 bit per page) to find all relevant pages. 29 | Results of this node are fed to the Bitmap Heap Scan.`, 30 | 'CTE SCAN': `performs a sequential scan of Common Table Expression (CTE) query results. Note that 31 | results of a CTE are materialized (calculated and temporarily stored).` 32 | 33 | }; 34 | -------------------------------------------------------------------------------- /app/services/plan-service.ts: -------------------------------------------------------------------------------- 1 | import {IPlan} from '../interfaces/iplan'; 2 | import {EstimateDirection} from '../enums'; 3 | /// 4 | /// 5 | 6 | export class PlanService { 7 | // plan property keys 8 | NODE_TYPE_PROP: string = 'Node Type'; 9 | ACTUAL_ROWS_PROP: string = 'Actual Rows'; 10 | PLAN_ROWS_PROP: string = 'Plan Rows'; 11 | ACTUAL_TOTAL_TIME_PROP: string = 'Actual Total Time'; 12 | ACTUAL_LOOPS_PROP: string = 'Actual Loops'; 13 | TOTAL_COST_PROP: string = 'Total Cost'; 14 | PLANS_PROP: string = 'Plans'; 15 | RELATION_NAME_PROP: string = 'Relation Name'; 16 | SCHEMA_PROP: string = 'Schema'; 17 | ALIAS_PROP: string = 'Alias'; 18 | GROUP_KEY_PROP: string = 'Group Key'; 19 | SORT_KEY_PROP: string = 'Sort Key'; 20 | JOIN_TYPE_PROP: string = 'Join Type'; 21 | INDEX_NAME_PROP: string = 'Index Name'; 22 | HASH_CONDITION_PROP: string = 'Hash Cond'; 23 | 24 | // computed by pev 25 | COMPUTED_TAGS_PROP: string = '*Tags'; 26 | 27 | COSTLIEST_NODE_PROP: string = '*Costiest Node (by cost)'; 28 | LARGEST_NODE_PROP: string = '*Largest Node (by rows)'; 29 | SLOWEST_NODE_PROP: string = '*Slowest Node (by duration)'; 30 | 31 | MAXIMUM_COSTS_PROP: string = '*Most Expensive Node (cost)'; 32 | MAXIMUM_ROWS_PROP: string = '*Largest Node (rows)'; 33 | MAXIMUM_DURATION_PROP: string = '*Slowest Node (time)'; 34 | ACTUAL_DURATION_PROP: string = '*Actual Duration'; 35 | ACTUAL_COST_PROP: string = '*Actual Cost'; 36 | PLANNER_ESTIMATE_FACTOR: string = '*Planner Row Estimate Factor'; 37 | PLANNER_ESIMATE_DIRECTION: string = '*Planner Row Estimate Direction'; 38 | 39 | CTE_SCAN_PROP = 'CTE Scan'; 40 | CTE_NAME_PROP = 'CTE Name'; 41 | 42 | ARRAY_INDEX_KEY: string = 'arrayIndex'; 43 | 44 | PEV_PLAN_TAG: string = 'plan_'; 45 | 46 | private _maxRows: number = 0; 47 | private _maxCost: number = 0; 48 | private _maxDuration: number = 0; 49 | 50 | getPlans(): Array { 51 | var plans: Array = []; 52 | 53 | for (var i in localStorage) { 54 | if (_.startsWith(i, this.PEV_PLAN_TAG)) { 55 | plans.push(JSON.parse(localStorage[i])); 56 | } 57 | } 58 | 59 | return _.chain(plans) 60 | .sortBy('createdOn') 61 | .reverse() 62 | .value(); 63 | } 64 | 65 | getPlan(id: string): IPlan { 66 | return JSON.parse(localStorage.getItem(id)); 67 | } 68 | 69 | createPlan(planName: string, planContent: string, planQuery): IPlan { 70 | var plan: IPlan = { 71 | id: this.PEV_PLAN_TAG + new Date().getTime().toString(), 72 | name: planName || 'plan created on ' + moment().format('LLL'), 73 | createdOn: new Date(), 74 | content: JSON.parse(planContent)[0], 75 | query: planQuery 76 | }; 77 | 78 | this.analyzePlan(plan); 79 | return plan; 80 | } 81 | 82 | isJsonString(str) { 83 | try { 84 | JSON.parse(str); 85 | } catch (e) { 86 | return false; 87 | } 88 | return true; 89 | } 90 | 91 | analyzePlan(plan: IPlan) { 92 | this.processNode(plan.content.Plan); 93 | plan.content[this.MAXIMUM_ROWS_PROP] = this._maxRows; 94 | plan.content[this.MAXIMUM_COSTS_PROP] = this._maxCost; 95 | plan.content[this.MAXIMUM_DURATION_PROP] = this._maxDuration; 96 | 97 | this.findOutlierNodes(plan.content.Plan); 98 | 99 | localStorage.setItem(plan.id, JSON.stringify(plan)); 100 | } 101 | 102 | deletePlan(plan: IPlan) { 103 | localStorage.removeItem(plan.id); 104 | } 105 | 106 | deleteAllPlans() { 107 | localStorage.clear(); 108 | } 109 | 110 | // recursively walk down the plan to compute various metrics 111 | processNode(node) { 112 | this.calculatePlannerEstimate(node); 113 | this.calculateActuals(node); 114 | 115 | _.each(node, (value, key) => { 116 | this.calculateMaximums(node, key, value); 117 | 118 | if (key === this.PLANS_PROP) { 119 | _.each(value, (value) => { 120 | this.processNode(value); 121 | }); 122 | } 123 | }); 124 | } 125 | 126 | calculateMaximums(node, key, value) { 127 | if (key === this.ACTUAL_ROWS_PROP && this._maxRows < value) { 128 | this._maxRows = value; 129 | } 130 | if (key === this.ACTUAL_COST_PROP && this._maxCost < value) { 131 | this._maxCost = value; 132 | } 133 | 134 | if (key === this.ACTUAL_DURATION_PROP && this._maxDuration < value) { 135 | this._maxDuration = value; 136 | } 137 | } 138 | 139 | findOutlierNodes(node) { 140 | node[this.SLOWEST_NODE_PROP] = false; 141 | node[this.LARGEST_NODE_PROP] = false; 142 | node[this.COSTLIEST_NODE_PROP] = false; 143 | 144 | if (node[this.ACTUAL_COST_PROP] === this._maxCost) { 145 | node[this.COSTLIEST_NODE_PROP] = true; 146 | } 147 | if (node[this.ACTUAL_ROWS_PROP] === this._maxRows) { 148 | node[this.LARGEST_NODE_PROP] = true; 149 | } 150 | if (node[this.ACTUAL_DURATION_PROP] === this._maxDuration) { 151 | node[this.SLOWEST_NODE_PROP] = true; 152 | } 153 | 154 | _.each(node, (value, key) => { 155 | if (key === this.PLANS_PROP) { 156 | _.each(value, (value) => { 157 | this.findOutlierNodes(value); 158 | }); 159 | } 160 | }); 161 | } 162 | 163 | // actual duration and actual cost are calculated by subtracting child values from the total 164 | calculateActuals(node) { 165 | node[this.ACTUAL_DURATION_PROP] = node[this.ACTUAL_TOTAL_TIME_PROP]; 166 | node[this.ACTUAL_COST_PROP] = node[this.TOTAL_COST_PROP]; 167 | 168 | console.log (node); 169 | _.each(node.Plans, subPlan => { 170 | console.log('processing chldren', subPlan) 171 | // since CTE scan duration is already included in its subnodes, it should be be 172 | // subtracted from the duration of this node 173 | if (subPlan[this.NODE_TYPE_PROP] !== this.CTE_SCAN_PROP) { 174 | node[this.ACTUAL_DURATION_PROP] = node[this.ACTUAL_DURATION_PROP] - subPlan[this.ACTUAL_TOTAL_TIME_PROP]; 175 | node[this.ACTUAL_COST_PROP] = node[this.ACTUAL_COST_PROP] - subPlan[this.TOTAL_COST_PROP]; 176 | } 177 | }); 178 | 179 | if (node[this.ACTUAL_COST_PROP] < 0) { 180 | node[this.ACTUAL_COST_PROP] = 0; 181 | } 182 | 183 | // since time is reported for an invidual loop, actual duration must be adjusted by number of loops 184 | node[this.ACTUAL_DURATION_PROP] = node[this.ACTUAL_DURATION_PROP] * node[this.ACTUAL_LOOPS_PROP]; 185 | } 186 | 187 | // figure out order of magnitude by which the planner mis-estimated how many rows would be 188 | // invloved in this node 189 | calculatePlannerEstimate(node) { 190 | node[this.PLANNER_ESTIMATE_FACTOR] = node[this.ACTUAL_ROWS_PROP] / node[this.PLAN_ROWS_PROP]; 191 | node[this.PLANNER_ESIMATE_DIRECTION] = EstimateDirection.under; 192 | 193 | if (node[this.PLANNER_ESTIMATE_FACTOR] < 1) { 194 | node[this.PLANNER_ESIMATE_DIRECTION] = EstimateDirection.over; 195 | node[this.PLANNER_ESTIMATE_FACTOR] = node[this.PLAN_ROWS_PROP] / node[this.ACTUAL_ROWS_PROP]; 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /app/services/syntax-highlight-service.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | export class SyntaxHighlightService { 5 | OPEN_TAG: string = ' _OPEN_TAG_'; 6 | CLOSE_TAG: string = '_CLOSE_TAG_'; 7 | 8 | highlight(code: string, keyItems: Array) { 9 | hljs.registerLanguage('sql', LANG_SQL); 10 | hljs.configure({ 11 | tabReplace: ' ' 12 | }); 13 | 14 | // prior to syntax highlighting, we want to tag key items in the raw code. making the 15 | // query upper case and ensuring that all comma separated values have a space 16 | // makes it simpler to find the items we're looing for 17 | var result: string = code.toUpperCase().replace(', ', ','); 18 | _.each(keyItems, (keyItem: string) => { 19 | result = result.replace(keyItem.toUpperCase(), `${this.OPEN_TAG}${keyItem}${this.CLOSE_TAG}`); 20 | }); 21 | 22 | result = hljs.highlightAuto(result).value; 23 | result = result.replace(new RegExp(this.OPEN_TAG, 'g'), ``); 24 | result = result.replace(new RegExp(this.CLOSE_TAG, 'g'), ''); 25 | 26 | return result; 27 | } 28 | } 29 | 30 | export var LANG_SQL = function(hljs) { 31 | var COMMENT_MODE = hljs.COMMENT('--', '$'); 32 | return { 33 | case_insensitive: true, 34 | illegal: /[<>{}*]/, 35 | contains: [ 36 | { 37 | beginKeywords: 38 | 'begin end start commit rollback savepoint lock alter create drop rename call ' + 39 | 'delete do handler insert load replace select truncate update set show pragma grant ' + 40 | 'merge describe use explain help declare prepare execute deallocate release ' + 41 | 'unlock purge reset change stop analyze cache flush optimize repair kill ' + 42 | 'install uninstall checksum restore check backup revoke', 43 | end: /;/, endsWithParent: true, 44 | keywords: { 45 | keyword: 46 | 'abort abs absolute acc acce accep accept access accessed accessible account acos action activate add ' + 47 | 'addtime admin administer advanced advise aes_decrypt aes_encrypt after agent aggregate ali alia alias ' + 48 | 'allocate allow alter always analyze ancillary and any anydata anydataset anyschema anytype apply ' + 49 | 'archive archived archivelog are as asc ascii asin assembly assertion associate asynchronous at atan ' + 50 | 'atn2 attr attri attrib attribu attribut attribute attributes audit authenticated authentication authid ' + 51 | 'authors auto autoallocate autodblink autoextend automatic availability avg backup badfile basicfile ' + 52 | 'before begin beginning benchmark between bfile bfile_base big bigfile bin binary_double binary_float ' + 53 | 'binlog bit_and bit_count bit_length bit_or bit_xor bitmap blob_base block blocksize body both bound ' + 54 | 'buffer_cache buffer_pool build bulk by byte byteordermark bytes cache caching call calling cancel ' + 55 | 'capacity cascade cascaded case cast catalog category ceil ceiling chain change changed char_base ' + 56 | 'char_length character_length characters characterset charindex charset charsetform charsetid check ' + 57 | 'checksum checksum_agg child choose chr chunk class cleanup clear client clob clob_base clone close ' + 58 | 'cluster_id cluster_probability cluster_set clustering coalesce coercibility col collate collation ' + 59 | 'collect colu colum column column_value columns columns_updated comment commit compact compatibility ' + 60 | 'compiled complete composite_limit compound compress compute concat concat_ws concurrent confirm conn ' + 61 | 'connec connect connect_by_iscycle connect_by_isleaf connect_by_root connect_time connection ' + 62 | 'consider consistent constant constraint constraints constructor container content contents context ' + 63 | 'contributors controlfile conv convert convert_tz corr corr_k corr_s corresponding corruption cos cost ' + 64 | 'count count_big counted covar_pop covar_samp cpu_per_call cpu_per_session crc32 create creation ' + 65 | 'critical cross cube cume_dist curdate current current_date current_time current_timestamp current_user ' + 66 | 'cursor curtime customdatum cycle d data database databases datafile datafiles datalength date_add ' + 67 | 'date_cache date_format date_sub dateadd datediff datefromparts datename datepart datetime2fromparts ' + 68 | 'day day_to_second dayname dayofmonth dayofweek dayofyear days db_role_change dbtimezone ddl deallocate ' + 69 | 'declare decode decompose decrement decrypt deduplicate def defa defau defaul default defaults ' + 70 | 'deferred defi defin define degrees delayed delegate delete delete_all delimited demand dense_rank ' + 71 | 'depth dequeue des_decrypt des_encrypt des_key_file desc descr descri describ describe descriptor ' + 72 | 'deterministic diagnostics difference dimension direct_load directory disable disable_all ' + 73 | 'disallow disassociate discardfile disconnect diskgroup distinct distinctrow distribute distributed div ' + 74 | 'do document domain dotnet double downgrade drop dumpfile duplicate duration e each edition editionable ' + 75 | 'editions element ellipsis else elsif elt empty enable enable_all enclosed encode encoding encrypt ' + 76 | 'end end-exec endian enforced engine engines enqueue enterprise entityescaping eomonth error errors ' + 77 | 'escaped evalname evaluate event eventdata events except exception exceptions exchange exclude excluding ' + 78 | 'execu execut execute exempt exists exit exp expire explain export export_set extended extent external ' + 79 | 'external_1 external_2 externally extract f failed failed_login_attempts failover failure far fast ' + 80 | 'feature_set feature_value fetch field fields file file_name_convert filesystem_like_logging final ' + 81 | 'finish first first_value fixed flash_cache flashback floor flush following follows for forall force ' + 82 | 'form forma format found found_rows freelist freelists freepools fresh from from_base64 from_days ' + 83 | 'ftp full function g general generated get get_format get_lock getdate getutcdate global global_name ' + 84 | 'globally go goto grant grants greatest group group_concat group_id grouping grouping_id groups ' + 85 | 'gtid_subtract guarantee guard handler hash hashkeys having hea head headi headin heading heap help hex ' + 86 | 'hierarchy high high_priority hosts hour http i id ident_current ident_incr ident_seed identified ' + 87 | 'identity idle_time if ifnull ignore iif ilike ilm immediate import in include including increment ' + 88 | 'index indexes indexing indextype indicator indices inet6_aton inet6_ntoa inet_aton inet_ntoa infile ' + 89 | 'initial initialized initially initrans inmemory inner innodb input insert install instance instantiable ' + 90 | 'instr interface interleaved intersect into invalidate invisible is is_free_lock is_ipv4 is_ipv4_compat ' + 91 | 'is_not is_not_null is_used_lock isdate isnull isolation iterate java join json json_exists ' + 92 | 'k keep keep_duplicates key keys kill l language large last last_day last_insert_id last_value lax lcase ' + 93 | 'lead leading least leaves left len lenght length less level levels library like like2 like4 likec limit ' + 94 | 'lines link list listagg little ln load load_file lob lobs local localtime localtimestamp locate ' + 95 | 'locator lock locked log log10 log2 logfile logfiles logging logical logical_reads_per_call ' + 96 | 'logoff logon logs long loop low low_priority lower lpad lrtrim ltrim m main make_set makedate maketime ' + 97 | 'managed management manual map mapping mask master master_pos_wait match matched materialized max ' + 98 | 'maxextents maximize maxinstances maxlen maxlogfiles maxloghistory maxlogmembers maxsize maxtrans ' + 99 | 'md5 measures median medium member memcompress memory merge microsecond mid migration min minextents ' + 100 | 'minimum mining minus minute minvalue missing mod mode model modification modify module monitoring month ' + 101 | 'months mount move movement multiset mutex n name name_const names nan national native natural nav nchar ' + 102 | 'nclob nested never new newline next nextval no no_write_to_binlog noarchivelog noaudit nobadfile ' + 103 | 'nocheck nocompress nocopy nocycle nodelay nodiscardfile noentityescaping noguarantee nokeep nologfile ' + 104 | 'nomapping nomaxvalue nominimize nominvalue nomonitoring none noneditionable nonschema noorder ' + 105 | 'nopr nopro noprom nopromp noprompt norely noresetlogs noreverse normal norowdependencies noschemacheck ' + 106 | 'noswitch not nothing notice notrim novalidate now nowait nth_value nullif nulls num numb numbe ' + 107 | 'nvarchar nvarchar2 object ocicoll ocidate ocidatetime ociduration ociinterval ociloblocator ocinumber ' + 108 | 'ociref ocirefcursor ocirowid ocistring ocitype oct octet_length of off offline offset oid oidindex old ' + 109 | 'on online only opaque open operations operator optimal optimize option optionally or oracle oracle_date ' + 110 | 'oradata ord ordaudio orddicom orddoc order ordimage ordinality ordvideo organization orlany orlvary ' + 111 | 'out outer outfile outline output over overflow overriding p package pad parallel parallel_enable ' + 112 | 'parameters parent parse partial partition partitions pascal passing password password_grace_time ' + 113 | 'password_lock_time password_reuse_max password_reuse_time password_verify_function patch path patindex ' + 114 | 'pctincrease pctthreshold pctused pctversion percent percent_rank percentile_cont percentile_disc ' + 115 | 'performance period period_add period_diff permanent physical pi pipe pipelined pivot pluggable plugin ' + 116 | 'policy position post_transaction pow power pragma prebuilt precedes preceding precision prediction ' + 117 | 'prediction_cost prediction_details prediction_probability prediction_set prepare present preserve ' + 118 | 'prior priority private private_sga privileges procedural procedure procedure_analyze processlist ' + 119 | 'profiles project prompt protection public publishingservername purge quarter query quick quiesce quota ' + 120 | 'quotename radians raise rand range rank raw read reads readsize rebuild record records ' + 121 | 'recover recovery recursive recycle redo reduced ref reference referenced references referencing refresh ' + 122 | 'regexp_like register regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy ' + 123 | 'reject rekey relational relative relaylog release release_lock relies_on relocate rely rem remainder rename ' + 124 | 'repair repeat replace replicate replication required reset resetlogs resize resource respect restore ' + 125 | 'restricted result result_cache resumable resume retention return returning returns reuse reverse revoke ' + 126 | 'right rlike role roles rollback rolling rollup round row row_count rowdependencies rowid rownum rows ' + 127 | 'rtrim rules safe salt sample save savepoint sb1 sb2 sb4 scan schema schemacheck scn scope scroll ' + 128 | 'sdo_georaster sdo_topo_geometry search sec_to_time second section securefile security seed segment select ' + 129 | 'self sequence sequential serializable server servererror session session_user sessions_per_user set ' + 130 | 'sets settings sha sha1 sha2 share shared shared_pool short show shrink shutdown si_averagecolor ' + 131 | 'si_colorhistogram si_featurelist si_positionalcolor si_stillimage si_texture siblings sid sign sin ' + 132 | 'size size_t sizes skip slave sleep smalldatetimefromparts smallfile snapshot some soname sort soundex ' + 133 | 'source space sparse spfile split sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows ' + 134 | 'sql_small_result sql_variant_property sqlcode sqldata sqlerror sqlname sqlstate sqrt square standalone ' + 135 | 'standby start starting startup statement static statistics stats_binomial_test stats_crosstab ' + 136 | 'stats_ks_test stats_mode stats_mw_test stats_one_way_anova stats_t_test_ stats_t_test_indep ' + 137 | 'stats_t_test_one stats_t_test_paired stats_wsr_test status std stddev stddev_pop stddev_samp stdev ' + 138 | 'stop storage store stored str str_to_date straight_join strcmp strict string struct stuff style subdate ' + 139 | 'subpartition subpartitions substitutable substr substring subtime subtring_index subtype success sum ' + 140 | 'suspend switch switchoffset switchover sync synchronous synonym sys sys_xmlagg sysasm sysaux sysdate ' + 141 | 'sysdatetimeoffset sysdba sysoper system system_user sysutcdatetime t table tables tablespace tan tdo ' + 142 | 'template temporary terminated tertiary_weights test than then thread through tier ties time time_format ' + 143 | 'time_zone timediff timefromparts timeout timestamp timestampadd timestampdiff timezone_abbr ' + 144 | 'timezone_minute timezone_region to to_base64 to_date to_days to_seconds todatetimeoffset trace tracking ' + 145 | 'transaction transactional translate translation treat trigger trigger_nestlevel triggers trim truncate ' + 146 | 'try_cast try_convert try_parse type ub1 ub2 ub4 ucase unarchived unbounded uncompress ' + 147 | 'under undo unhex unicode uniform uninstall union unique unix_timestamp unknown unlimited unlock unpivot ' + 148 | 'unrecoverable unsafe unsigned until untrusted unusable unused update updated upgrade upped upper upsert ' + 149 | 'url urowid usable usage use use_stored_outlines user user_data user_resources users using utc_date ' + 150 | 'utc_timestamp uuid uuid_short validate validate_password_strength validation valist value values var ' + 151 | 'var_samp varcharc vari varia variab variabl variable variables variance varp varraw varrawc varray ' + 152 | 'verify version versions view virtual visible void wait wallet warning warnings week weekday weekofyear ' + 153 | 'wellformed when whene whenev wheneve whenever where while whitespace with within without work wrapped ' + 154 | 'xdb xml xmlagg xmlattributes xmlcast xmlcolattval xmlelement xmlexists xmlforest xmlindex xmlnamespaces ' + 155 | 'xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltype xor year year_to_month years yearweek', 156 | literal: 157 | 'true false null', 158 | built_in: 159 | 'array bigint binary bit blob boolean char character date dec decimal float int int8 integer interval number ' + 160 | 'numeric real record serial serial8 smallint text varchar varying void' 161 | }, 162 | contains: [ 163 | { 164 | className: 'string', 165 | begin: '\'', end: '\'', 166 | contains: [hljs.BACKSLASH_ESCAPE, { begin: '\'\'' }] 167 | }, 168 | { 169 | className: 'string', 170 | begin: '"', end: '"', 171 | contains: [hljs.BACKSLASH_ESCAPE, { begin: '""' }] 172 | }, 173 | { 174 | className: 'string', 175 | begin: '`', end: '`', 176 | contains: [hljs.BACKSLASH_ESCAPE] 177 | }, 178 | hljs.C_NUMBER_MODE, 179 | hljs.C_BLOCK_COMMENT_MODE, 180 | COMMENT_MODE 181 | ] 182 | }, 183 | hljs.C_BLOCK_COMMENT_MODE, 184 | COMMENT_MODE 185 | ] 186 | }; 187 | }; 188 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | pev: 2 | build: . 3 | ports: 4 | - "8000:8000" 5 | volumes: 6 | - ./:/var/www/pev/ 7 | -------------------------------------------------------------------------------- /gulpfile.ts: -------------------------------------------------------------------------------- 1 | import * as gulp from 'gulp'; 2 | import {runSequence, task} from './tools/utils'; 3 | 4 | // -------------- 5 | // Clean (override). 6 | gulp.task('clean', task('clean', 'all')); 7 | gulp.task('clean.dist', task('clean', 'dist')); 8 | gulp.task('clean.test', task('clean', 'test')); 9 | gulp.task('clean.tmp', task('clean', 'tmp')); 10 | 11 | gulp.task('check.versions', task('check.versions')); 12 | 13 | // -------------- 14 | // Postinstall. 15 | gulp.task('postinstall', done => 16 | runSequence('clean', 17 | 'npm', 18 | done)); 19 | 20 | // -------------- 21 | // Build dev. 22 | gulp.task('build.dev', done => 23 | runSequence('clean.dist', 24 | 'tslint', 25 | 'build.sass.dev', 26 | 'build.img.dev', 27 | 'build.fonts.dev', 28 | 'build.js.dev', 29 | 'build.index', 30 | done)); 31 | 32 | // -------------- 33 | // Build prod. 34 | gulp.task('build.prod', done => 35 | runSequence('clean.dist', 36 | 'clean.tmp', 37 | 'tslint', 38 | 'build.sass.dev', 39 | 'build.img.dev', 40 | 'build.fonts.dev', 41 | 'build.html_css.prod', 42 | 'build.deps', 43 | 'build.js.prod', 44 | 'build.bundles', 45 | 'build.index', 46 | done)); 47 | 48 | // -------------- 49 | // Watch. 50 | gulp.task('build.dev.watch', done => 51 | runSequence('build.dev', 52 | 'watch.dev', 53 | done)); 54 | 55 | gulp.task('build.test.watch', done => 56 | runSequence('build.test', 57 | 'watch.test', 58 | done)); 59 | 60 | // -------------- 61 | // Test. 62 | gulp.task('test', done => 63 | runSequence('clean.test', 64 | 'tslint', 65 | 'build.test', 66 | 'karma.start', 67 | done)); 68 | 69 | // -------------- 70 | // Serve. 71 | gulp.task('serve', done => 72 | runSequence('build.dev', 73 | 'server.start', 74 | 'watch.serve', 75 | done)); 76 | 77 | // -------------- 78 | // Docs 79 | // Disabled until https://github.com/sebastian-lenz/typedoc/issues/162 gets resolved 80 | // gulp.task('docs', done => 81 | // runSequence('build.docs', 82 | // 'serve.docs', 83 | // done)); 84 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Wed Jul 15 2015 09:44:02 GMT+0200 (Romance Daylight Time) 3 | 'use strict'; 4 | 5 | module.exports = function(config) { 6 | config.set({ 7 | 8 | // base path that will be used to resolve all patterns (eg. files, exclude) 9 | basePath: './', 10 | 11 | 12 | // frameworks to use 13 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 14 | frameworks: ['jasmine'], 15 | 16 | 17 | // list of files / patterns to load in the browser 18 | files: [ 19 | 'node_modules/zone.js/dist/zone-microtask.js', 20 | 'node_modules/zone.js/dist/long-stack-trace-zone.js', 21 | 'node_modules/zone.js/dist/jasmine-patch.js', 22 | 'node_modules/es6-module-loader/dist/es6-module-loader.js', 23 | 'node_modules/traceur/bin/traceur-runtime.js', // Required by PhantomJS2, otherwise it shouts ReferenceError: Can't find variable: require 24 | 'node_modules/traceur/bin/traceur.js', 25 | 'node_modules/systemjs/dist/system.src.js', 26 | 'node_modules/reflect-metadata/Reflect.js', 27 | 28 | { pattern: 'node_modules/angular2/**/*.js', included: false, watched: false }, 29 | { pattern: 'node_modules/rxjs/**/*.js', included: false, watched: false }, 30 | { pattern: 'test/**/*.js', included: false, watched: true }, 31 | { pattern: 'node_modules/systemjs/dist/system-polyfills.js', included: false, watched: false }, // PhantomJS2 (and possibly others) might require it 32 | 33 | 'test-main.js' 34 | ], 35 | 36 | 37 | // list of files to exclude 38 | exclude: [ 39 | 'node_modules/angular2/**/*_spec.js' 40 | ], 41 | 42 | 43 | // preprocess matching files before serving them to the browser 44 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 45 | preprocessors: { 46 | }, 47 | 48 | 49 | // test results reporter to use 50 | // possible values: 'dots', 'progress' 51 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 52 | reporters: ['mocha'], 53 | 54 | 55 | // web server port 56 | port: 9876, 57 | 58 | 59 | // enable / disable colors in the output (reporters and logs) 60 | colors: true, 61 | 62 | 63 | // level of logging 64 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 65 | logLevel: config.LOG_INFO, 66 | 67 | 68 | // enable / disable watching file and executing tests whenever any file changes 69 | autoWatch: true, 70 | 71 | 72 | // start these browsers 73 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 74 | browsers: [ 75 | 'PhantomJS2', 76 | 'Chrome' 77 | ], 78 | 79 | 80 | customLaunchers: { 81 | Chrome_travis_ci: { 82 | base: 'Chrome', 83 | flags: ['--no-sandbox'] 84 | } 85 | }, 86 | 87 | 88 | // Continuous Integration mode 89 | // if true, Karma captures browsers, runs the tests and exits 90 | singleRun: false 91 | }); 92 | 93 | if (process.env.APPVEYOR) { 94 | config.browsers = ['IE']; 95 | config.singleRun = true; 96 | config.browserNoActivityTimeout = 90000; // Note: default value (10000) is not enough 97 | } 98 | 99 | if (process.env.TRAVIS || process.env.CIRCLECI) { 100 | config.browsers = ['Chrome_travis_ci']; 101 | config.singleRun = true; 102 | } 103 | }; 104 | -------------------------------------------------------------------------------- /logo.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AlexTatiyants/pev/6d31cdd75f557761d7581da6c46586792e5f2dad/logo.ai -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pev", 3 | "version": "0.0.0", 4 | "description": "Postgres Explain Visualizer", 5 | "repository": { 6 | "url": "http://www.tatiyants.com" 7 | }, 8 | "scripts": { 9 | "build.dev": "gulp build.dev", 10 | "build.dev.watch": "gulp build.dev.watch", 11 | "build.prod": "gulp build.prod --env prod", 12 | "build.test": "gulp build.test", 13 | "build.test.watch": "gulp build.test.watch", 14 | "docs": "npm run gulp -- build.docs && npm run gulp -- serve.docs", 15 | "gulp": "gulp", 16 | "karma": "karma", 17 | "karma.start": "karma start", 18 | "lint": "gulp tslint", 19 | "postinstall": "tsd reinstall --clean && tsd link && tsd rebundle && gulp check.versions && gulp postinstall", 20 | "reinstall": "rimraf node_modules && npm cache clean && npm install", 21 | "start": "gulp serve --env dev", 22 | "serve.dev": "gulp serve --env dev", 23 | "tasks.list": "gulp --tasks-simple", 24 | "test": "gulp test", 25 | "tsd": "tsd" 26 | }, 27 | "author": "Alex Tatiyants", 28 | "license": "MIT", 29 | "devDependencies": { 30 | "async": "^1.4.2", 31 | "chalk": "^1.1.1", 32 | "connect-livereload": "^0.5.3", 33 | "del": "^1.1.1", 34 | "express": "~4.13.1", 35 | "extend": "^3.0.0", 36 | "gulp": "^3.9.0", 37 | "gulp-compass": "^2.1.0", 38 | "gulp-concat": "^2.5.2", 39 | "gulp-filter": "^2.0.2", 40 | "gulp-inject": "^1.3.1", 41 | "gulp-inline-ng2-template": "^0.0.7", 42 | "gulp-load-plugins": "^0.10.0", 43 | "gulp-minify-css": "^1.1.6", 44 | "gulp-minify-html": "^1.0.3", 45 | "gulp-plumber": "~1.0.1", 46 | "gulp-sass": "^2.0.4", 47 | "gulp-shell": "~0.4.3", 48 | "gulp-sourcemaps": "~1.5.2", 49 | "gulp-template": "^3.0.0", 50 | "gulp-tslint": "^3.3.0", 51 | "gulp-tslint-stylish": "^1.0.4", 52 | "gulp-typescript": "~2.8.2", 53 | "gulp-uglify": "^1.2.0", 54 | "gulp-util": "^3.0.7", 55 | "gulp-watch": "^4.2.4", 56 | "jasmine-core": "~2.3.4", 57 | "karma": "~0.13.15", 58 | "karma-chrome-launcher": "~0.2.0", 59 | "karma-ie-launcher": "^0.2.0", 60 | "karma-jasmine": "~0.3.6", 61 | "karma-mocha-reporter": "^1.1.1", 62 | "karma-phantomjs2-launcher": "^0.3.2", 63 | "merge-stream": "^1.0.0", 64 | "open": "0.0.5", 65 | "rimraf": "^2.4.3", 66 | "run-sequence": "^1.1.0", 67 | "semver": "^5.0.3", 68 | "serve-static": "^1.9.2", 69 | "slash": "~1.0.0", 70 | "stream-series": "^0.1.1", 71 | "systemjs-builder": "^0.14.8", 72 | "tiny-lr": "^0.2.1", 73 | "traceur": "^0.0.91", 74 | "ts-node": "^0.5.4", 75 | "tsd": "^0.6.4", 76 | "typedoc": "^0.3.12", 77 | "typescript": "~1.7.3", 78 | "typescript-register": "^1.1.0", 79 | "yargs": "^3.25.0" 80 | }, 81 | "dependencies": { 82 | "angular2": "2.0.0-beta.0", 83 | "bootstrap": "^3.3.5", 84 | "es6-module-loader": "^0.17.8", 85 | "es6-shim": "^0.33.3", 86 | "highlight.js": "^9.1.0", 87 | "lodash": "^3.10.1", 88 | "moment": "^2.10.6", 89 | "reflect-metadata": "0.1.2", 90 | "rxjs": "5.0.0-beta.0", 91 | "systemjs": "^0.19.4", 92 | "zone.js": "0.5.10" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /test-main.js: -------------------------------------------------------------------------------- 1 | // Turn on full stack traces in errors to help debugging 2 | Error.stackTraceLimit=Infinity; 3 | 4 | jasmine.DEFAULT_TIMEOUT_INTERVAL = 100; 5 | 6 | // Cancel Karma's synchronous start, 7 | // we will call `__karma__.start()` later, once all the specs are loaded. 8 | __karma__.loaded = function() {}; 9 | 10 | System.config({ 11 | baseURL: '/base/', 12 | defaultJSExtensions: true, 13 | paths: { 14 | 'angular2/*': 'node_modules/angular2/*.js', 15 | 'rxjs/*': 'node_modules/rxjs/*.js' 16 | } 17 | }); 18 | 19 | System.import('angular2/src/platform/browser/browser_adapter').then(function(browser_adapter) { 20 | browser_adapter.BrowserDomAdapter.makeCurrent(); 21 | }).then(function() { 22 | return Promise.all( 23 | Object.keys(window.__karma__.files) // All files served by Karma. 24 | .filter(onlySpecFiles) 25 | .map(file2moduleName) 26 | .map(function(path) { 27 | return System.import(path).then(function(module) { 28 | if (module.hasOwnProperty('main')) { 29 | module.main(); 30 | } else { 31 | throw new Error('Module ' + path + ' does not implement main() method.'); 32 | } 33 | }); 34 | })); 35 | }) 36 | .then(function() { 37 | __karma__.start(); 38 | }, function(error) { 39 | console.error(error.stack || error); 40 | __karma__.start(); 41 | }); 42 | 43 | 44 | function onlySpecFiles(path) { 45 | return /[\.|_]spec\.js$/.test(path); 46 | } 47 | 48 | // Normalize paths to module names. 49 | function file2moduleName(filePath) { 50 | return filePath.replace(/\\/g, '/') 51 | .replace(/^\/base\//, '') 52 | .replace(/\.js/, ''); 53 | } 54 | -------------------------------------------------------------------------------- /tools/config.ts: -------------------------------------------------------------------------------- 1 | import {readFileSync} from 'fs'; 2 | import {argv} from 'yargs'; 3 | 4 | // -------------- 5 | // Configuration. 6 | export const ENV = argv['env'] || 'dev'; 7 | export const DEBUG = argv['debug'] || false; 8 | export const PORT = argv['port'] || 5555; 9 | export const LIVE_RELOAD_PORT = argv['reload-port'] || 4002; 10 | export const DOCS_PORT = argv['docs-port'] || 4003; 11 | export const APP_BASE = argv['base'] || '/'; 12 | 13 | export const APP_TITLE = 'Postgres EXPLAIN Visualizer (pev)'; 14 | 15 | export const APP_SRC = 'app'; 16 | export const ASSETS_SRC = `${APP_SRC}/assets`; 17 | 18 | export const TOOLS_DIR = 'tools'; 19 | export const TMP_DIR = 'tmp'; 20 | export const TEST_DEST = 'test'; 21 | export const DOCS_DEST = 'docs'; 22 | export const APP_DEST = `dist/${ENV}`; 23 | export const ASSETS_DEST = `${APP_DEST}/assets`; 24 | export const BUNDLES_DEST = `${APP_DEST}/bundles`; 25 | export const CSS_DEST = `${APP_DEST}/css`; 26 | export const FONTS_DEST = `${APP_DEST}/fonts`; 27 | export const LIB_DEST = `${APP_DEST}/lib`; 28 | export const APP_ROOT = ENV === 'dev' ? `${APP_BASE}${APP_DEST}/` : `${APP_BASE}`; 29 | export const VERSION = appVersion(); 30 | 31 | export const VERSION_NPM = '2.14.7'; 32 | export const VERSION_NODE = '4.0.0'; 33 | 34 | // Declare NPM dependencies (Note that globs should not be injected). 35 | export const NPM_DEPENDENCIES = [ 36 | { src: 'systemjs/dist/system-polyfills.js', dest: LIB_DEST }, 37 | 38 | { src: 'es6-shim/es6-shim.min.js', inject: 'shims', dest: LIB_DEST }, 39 | { src: 'reflect-metadata/Reflect.js', inject: 'shims', dest: LIB_DEST }, 40 | { src: 'systemjs/dist/system.src.js', inject: 'shims', dest: LIB_DEST }, 41 | { src: 'angular2/bundles/angular2-polyfills.js', inject: 'shims', dest: LIB_DEST }, 42 | 43 | // Faster dev page load 44 | { src: 'rxjs/bundles/Rx.min.js', inject: 'libs', dest: LIB_DEST }, 45 | { src: 'angular2/bundles/angular2.min.js', inject: 'libs', dest: LIB_DEST }, 46 | { src: 'angular2/bundles/router.js', inject: 'libs', dest: LIB_DEST }, // use router.min.js with alpha47 47 | { src: 'angular2/bundles/http.min.js', inject: 'libs', dest: LIB_DEST }, 48 | 49 | { src: 'lodash/index.js', inject: 'libs', dest: LIB_DEST }, 50 | { src: 'moment/moment.js', inject: 'libs', dest: LIB_DEST }, 51 | { src: 'highlight.js/lib/highlight.js', inject: 'libs', dest: LIB_DEST }, 52 | ]; 53 | 54 | // Declare local files that needs to be injected 55 | export const APP_ASSETS = [ 56 | { src: `${ASSETS_SRC}/css/styles.css`, inject: true, dest: CSS_DEST } 57 | ]; 58 | 59 | NPM_DEPENDENCIES 60 | .filter(d => !/\*/.test(d.src)) // Skip globs 61 | .forEach(d => d.src = require.resolve(d.src)); 62 | 63 | export const DEPENDENCIES = NPM_DEPENDENCIES.concat(APP_ASSETS); 64 | 65 | // ---------------- 66 | // SystemsJS Configuration. 67 | const SYSTEM_CONFIG_DEV = { 68 | defaultJSExtensions: true, 69 | paths: { 70 | 'bootstrap': `${APP_ROOT}bootstrap`, 71 | '*': `${APP_BASE}node_modules/*` 72 | } 73 | }; 74 | 75 | const SYSTEM_CONFIG_PROD = { 76 | defaultJSExtensions: true, 77 | bundles: { 78 | 'bundles/app': ['bootstrap'] 79 | } 80 | }; 81 | 82 | export const SYSTEM_CONFIG = ENV === 'dev' ? SYSTEM_CONFIG_DEV : SYSTEM_CONFIG_PROD; 83 | 84 | // This is important to keep clean module names as 'module name == module uri'. 85 | export const SYSTEM_CONFIG_BUILDER = { 86 | defaultJSExtensions: true, 87 | paths: { 88 | '*': `${TMP_DIR}/*`, 89 | 'angular2/*': 'node_modules/angular2/*', 90 | 'rxjs/*': 'node_modules/rxjs/*' 91 | } 92 | }; 93 | 94 | // -------------- 95 | // Private. 96 | function appVersion(): number | string { 97 | var pkg = JSON.parse(readFileSync('package.json').toString()); 98 | return pkg.version; 99 | } 100 | -------------------------------------------------------------------------------- /tools/tasks/build.bundles.ts: -------------------------------------------------------------------------------- 1 | import {parallel} from 'async'; 2 | import {join} from 'path'; 3 | import * as Builder from 'systemjs-builder'; 4 | import {BUNDLES_DEST, SYSTEM_CONFIG_BUILDER} from '../config'; 5 | 6 | const BUNDLE_OPTS = { 7 | minify: true, 8 | sourceMaps: true, 9 | format: 'cjs' 10 | }; 11 | 12 | export = function bundles(gulp, plugins) { 13 | return function (done) { 14 | let builder = new Builder(SYSTEM_CONFIG_BUILDER); 15 | 16 | parallel([ 17 | bundleApp 18 | ], () => done()); 19 | 20 | function bundleApp(done) { 21 | builder.bundle( 22 | 'bootstrap - angular2/*', 23 | join(BUNDLES_DEST, 'app.js'), BUNDLE_OPTS).then(done); 24 | } 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /tools/tasks/build.deps.ts: -------------------------------------------------------------------------------- 1 | import * as merge from 'merge-stream'; 2 | import {DEPENDENCIES} from '../config'; 3 | 4 | export = function buildDepsProd(gulp, plugins) { 5 | return function () { 6 | let stream = merge(); 7 | 8 | DEPENDENCIES.forEach(dep => { 9 | stream.add(addStream(dep)); 10 | }); 11 | 12 | return stream; 13 | 14 | function addStream(dep) { 15 | let stream = gulp.src(dep.src); 16 | stream.pipe(gulp.dest(dep.dest)); 17 | return stream; 18 | } 19 | }; 20 | }; 21 | -------------------------------------------------------------------------------- /tools/tasks/build.docs.ts: -------------------------------------------------------------------------------- 1 | import {join} from 'path'; 2 | import {APP_SRC, APP_TITLE, DOCS_DEST} from '../config'; 3 | 4 | export = function buildDocs(gulp, plugins, option) { 5 | return function() { 6 | 7 | let src = [ 8 | join(APP_SRC, '**/*.ts'), 9 | '!' + join(APP_SRC, '**/*_spec.ts') 10 | ]; 11 | 12 | return gulp.src(src) 13 | .pipe(plugins.typedoc({ 14 | // TypeScript options (see typescript docs) 15 | module: 'commonjs', 16 | target: 'es5', 17 | includeDeclarations: true, 18 | // Output options (see typedoc docs) 19 | out: DOCS_DEST, 20 | json: join(DOCS_DEST , 'data/docs.json' ), 21 | name: APP_TITLE, 22 | ignoreCompilerErrors: false, 23 | experimentalDecorators: true, 24 | version: true 25 | })); 26 | }; 27 | } 28 | -------------------------------------------------------------------------------- /tools/tasks/build.fonts.dev.ts: -------------------------------------------------------------------------------- 1 | import {join} from 'path'; 2 | import {APP_SRC, APP_DEST} from '../config'; 3 | 4 | export = function buildFontsDev(gulp, plugins) { 5 | return function () { 6 | return gulp.src([ 7 | join(APP_SRC, '**/*.eot'), 8 | join(APP_SRC, '**/*.ttf'), 9 | join(APP_SRC, '**/*.woff'), 10 | join(APP_SRC, '**/*.woff2'), 11 | join(APP_SRC, '**/*.otf') 12 | ]) 13 | .pipe(gulp.dest(APP_DEST)); 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /tools/tasks/build.html_css.prod.ts: -------------------------------------------------------------------------------- 1 | import * as merge from 'merge-stream'; 2 | import {join} from 'path'; 3 | import {APP_SRC, TMP_DIR} from '../config'; 4 | 5 | // const HTML_MINIFIER_OPTS = { empty: true }; 6 | 7 | export = function buildJSDev(gulp, plugins) { 8 | return function () { 9 | 10 | return merge(minifyHtml(), minifyCss()); 11 | 12 | function minifyHtml() { 13 | return gulp.src(join(APP_SRC, '**/*.html')) 14 | // .pipe(plugins.minifyHtml(HTML_MINIFIER_OPTS)) 15 | .pipe(gulp.dest(TMP_DIR)); 16 | } 17 | 18 | function minifyCss() { 19 | return gulp.src(join(APP_SRC, '**/*.css')) 20 | .pipe(plugins.minifyCss()) 21 | .pipe(gulp.dest(TMP_DIR)); 22 | } 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /tools/tasks/build.img.dev.ts: -------------------------------------------------------------------------------- 1 | import {join} from 'path'; 2 | import {APP_SRC, APP_DEST} from '../config'; 3 | 4 | export = function buildImagesDev(gulp, plugins) { 5 | return function () { 6 | return gulp.src([ 7 | join(APP_SRC, '**/*.gif'), 8 | join(APP_SRC, '**/*.jpg'), 9 | join(APP_SRC, '**/*.png'), 10 | join(APP_SRC, '**/*.svg') 11 | ]) 12 | .pipe(gulp.dest(APP_DEST)); 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /tools/tasks/build.index.ts: -------------------------------------------------------------------------------- 1 | import {join, sep} from 'path'; 2 | import {APP_SRC, APP_DEST, DEPENDENCIES, ENV} from '../config'; 3 | import {transformPath, templateLocals} from '../utils'; 4 | 5 | export = function buildIndexDev(gulp, plugins) { 6 | return function () { 7 | return gulp.src(join(APP_SRC, 'index.html')) 8 | // NOTE: There might be a way to pipe in loop. 9 | .pipe(inject('shims')) 10 | .pipe(inject('libs')) 11 | .pipe(inject()) 12 | .pipe(plugins.template(templateLocals())) 13 | .pipe(gulp.dest(APP_DEST)); 14 | }; 15 | 16 | function inject(name?: string) { 17 | return plugins.inject(gulp.src(getInjectablesDependenciesRef(name), { read: false }), { 18 | name, 19 | transform: transformPath(plugins, 'dev') 20 | }); 21 | } 22 | 23 | function getInjectablesDependenciesRef(name?: string) { 24 | return DEPENDENCIES 25 | .filter(dep => dep['inject'] && dep['inject'] === (name || true)) 26 | .map(mapPath); 27 | } 28 | 29 | function mapPath(dep) { 30 | let prodPath = join(dep.dest, dep.src.split(sep).pop()); 31 | return ('prod' === ENV ? prodPath : dep.src ); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /tools/tasks/build.js.dev.ts: -------------------------------------------------------------------------------- 1 | import {join} from 'path'; 2 | import {APP_SRC, APP_DEST} from '../config'; 3 | import {templateLocals, tsProjectFn} from '../utils'; 4 | 5 | export = function buildJSDev(gulp, plugins) { 6 | let tsProject = tsProjectFn(plugins); 7 | return function () { 8 | let src = [ 9 | join(APP_SRC, '**/*.ts'), 10 | '!' + join(APP_SRC, '**/*_spec.ts') 11 | ]; 12 | 13 | let result = gulp.src(src) 14 | .pipe(plugins.plumber()) 15 | // Won't be required for non-production build after the change 16 | .pipe(plugins.inlineNg2Template({ base: APP_SRC })) 17 | .pipe(plugins.sourcemaps.init()) 18 | .pipe(plugins.typescript(tsProject)); 19 | 20 | return result.js 21 | .pipe(plugins.sourcemaps.write()) 22 | .pipe(plugins.template(templateLocals())) 23 | .pipe(gulp.dest(APP_DEST)); 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /tools/tasks/build.js.prod.ts: -------------------------------------------------------------------------------- 1 | import {join} from 'path'; 2 | import {APP_SRC, TMP_DIR} from '../config'; 3 | import {templateLocals, tsProjectFn} from '../utils'; 4 | 5 | export = function buildJSDev(gulp, plugins) { 6 | return function () { 7 | let tsProject = tsProjectFn(plugins); 8 | let src = [ 9 | join(APP_SRC, '**/*.ts'), 10 | '!' + join(APP_SRC, '**/*_spec.ts') 11 | ]; 12 | 13 | let result = gulp.src(src) 14 | .pipe(plugins.plumber()) 15 | .pipe(plugins.inlineNg2Template({ base: TMP_DIR })) 16 | .pipe(plugins.typescript(tsProject)); 17 | 18 | return result.js 19 | .pipe(plugins.template(templateLocals())) 20 | .pipe(gulp.dest(TMP_DIR)); 21 | }; 22 | }; 23 | -------------------------------------------------------------------------------- /tools/tasks/build.sass.dev.ts: -------------------------------------------------------------------------------- 1 | import {join} from 'path'; 2 | import {APP_SRC} from '../config'; 3 | 4 | export = function buildSassDev(gulp, plugins, option) { 5 | return function() { 6 | return gulp.src(join(APP_SRC, '**', '*.scss')) 7 | .pipe(plugins.plumber({ 8 | // this allows gulp not to crash on sass compilation errors 9 | errorHandler: function(error) { 10 | console.log(error.message); 11 | this.emit('end'); 12 | } 13 | })) 14 | .pipe(plugins.compass({ 15 | style: 'compressed', 16 | css: 'app/assets/css', 17 | sass: join(APP_SRC, 'assets/sass') 18 | })) 19 | .pipe(gulp.dest(join(APP_SRC, 'assets'))); 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /tools/tasks/build.test.ts: -------------------------------------------------------------------------------- 1 | import {join} from 'path'; 2 | import {APP_SRC, TEST_DEST} from '../config'; 3 | import {tsProjectFn} from '../utils'; 4 | 5 | export = function buildTest(gulp, plugins) { 6 | return function () { 7 | let tsProject = tsProjectFn(plugins); 8 | let src = [ 9 | join(APP_SRC, '**/*.ts'), 10 | '!' + join(APP_SRC, 'bootstrap.ts') 11 | ]; 12 | 13 | let result = gulp.src(src) 14 | .pipe(plugins.plumber()) 15 | .pipe(plugins.inlineNg2Template({ base: APP_SRC })) 16 | .pipe(plugins.typescript(tsProject)); 17 | 18 | return result.js 19 | .pipe(gulp.dest(TEST_DEST)); 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /tools/tasks/check.versions.ts: -------------------------------------------------------------------------------- 1 | import {VERSION_NPM, VERSION_NODE} from '../config'; 2 | 3 | function reportError(message: string) { 4 | console.error(require('chalk').white.bgRed.bold(message)); 5 | process.exit(1); 6 | } 7 | 8 | module.exports = function check(gulp, plugins) { 9 | return function () { 10 | let exec = require('child_process').exec; 11 | let semver = require('semver'); 12 | 13 | exec('npm --version', 14 | function (error, stdout, stderr) { 15 | if (error !== null) { 16 | reportError('npm preinstall error: ' + error + stderr); 17 | } 18 | 19 | if (!semver.gte(stdout, VERSION_NPM)) { 20 | reportError('NPM is not in required version! Required is ' + VERSION_NPM + ' and you\'re using ' + stdout); 21 | } 22 | }); 23 | 24 | exec('node --version', 25 | function (error, stdout, stderr) { 26 | if (error !== null) { 27 | reportError('npm preinstall error: ' + error + stderr); 28 | } 29 | 30 | if (!semver.gte(stdout, VERSION_NODE)) { 31 | reportError('NODE is not in required version! Required is ' + VERSION_NODE + ' and you\'re using ' + stdout); 32 | } 33 | }); 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /tools/tasks/clean.ts: -------------------------------------------------------------------------------- 1 | import * as async from 'async'; 2 | import * as del from 'del'; 3 | import {APP_DEST, TEST_DEST, TMP_DIR} from '../config'; 4 | 5 | export = function clean(gulp, plugins, option) { 6 | return function (done) { 7 | 8 | switch(option) { 9 | case 'all' : cleanAll(done); break; 10 | case 'dist' : cleanDist(done); break; 11 | case 'test' : cleanTest(done); break; 12 | case 'tmp' : cleanTmp(done); break; 13 | default: done(); 14 | } 15 | 16 | }; 17 | }; 18 | 19 | function cleanAll(done) { 20 | async.parallel([ 21 | cleanDist, 22 | cleanTest, 23 | cleanTmp 24 | ], done); 25 | } 26 | function cleanDist(done) { 27 | del(APP_DEST, done); 28 | } 29 | function cleanTest(done) { 30 | del(TEST_DEST, done); 31 | } 32 | function cleanTmp(done) { 33 | del(TMP_DIR, done); 34 | } 35 | -------------------------------------------------------------------------------- /tools/tasks/karma.start.ts: -------------------------------------------------------------------------------- 1 | import * as karma from 'karma'; 2 | import {join} from 'path'; 3 | 4 | export = function karmaStart() { 5 | return function (done) { 6 | new (karma).Server({ 7 | configFile: join(process.cwd(), 'karma.conf.js'), 8 | singleRun: true 9 | }).start(done); 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /tools/tasks/npm.ts: -------------------------------------------------------------------------------- 1 | export = function npm(gulp, plugins) { 2 | return plugins.shell.task([ 3 | 'npm prune' 4 | ]); 5 | }; 6 | -------------------------------------------------------------------------------- /tools/tasks/serve.docs.ts: -------------------------------------------------------------------------------- 1 | import {serveDocs} from '../utils'; 2 | 3 | export = function serverStart(gulp, plugins) { 4 | return function () { 5 | serveDocs(); 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /tools/tasks/server.start.ts: -------------------------------------------------------------------------------- 1 | import {serveSPA} from '../utils'; 2 | 3 | export = function serverStart(gulp, plugins) { 4 | return function () { 5 | serveSPA(); 6 | }; 7 | }; 8 | -------------------------------------------------------------------------------- /tools/tasks/tsd.ts: -------------------------------------------------------------------------------- 1 | export = function tsd(gulp, plugins) { 2 | return plugins.shell.task([ 3 | 'tsd reinstall --clean', 4 | 'tsd link', 5 | 'tsd rebundle' 6 | ]); 7 | }; 8 | -------------------------------------------------------------------------------- /tools/tasks/tslint.ts: -------------------------------------------------------------------------------- 1 | import {join} from 'path'; 2 | import {APP_SRC, TOOLS_DIR} from '../config'; 3 | 4 | export = function tslint(gulp, plugins) { 5 | return function () { 6 | let src = [ 7 | join(APP_SRC, '**/*.ts'), 8 | '!' + join(APP_SRC, '**/*.d.ts'), 9 | join(TOOLS_DIR, '**/*.ts'), 10 | '!' + join(TOOLS_DIR, '**/*.d.ts') 11 | ]; 12 | 13 | return gulp.src(src) 14 | .pipe(plugins.tslint()) 15 | .pipe(plugins.tslint.report(plugins.tslintStylish, { 16 | emitError: false, 17 | sort: true, 18 | bell: true 19 | })); 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /tools/tasks/watch.dev.ts: -------------------------------------------------------------------------------- 1 | import {join} from 'path'; 2 | import {APP_SRC} from '../config'; 3 | 4 | export = function watchDev(gulp, plugins) { 5 | return function () { 6 | plugins.watch(join(APP_SRC, '**/*'), () => gulp.start('build.dev')); 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /tools/tasks/watch.serve.ts: -------------------------------------------------------------------------------- 1 | import * as runSequence from 'run-sequence'; 2 | import {join} from 'path'; 3 | import {APP_SRC} from '../config'; 4 | import {notifyLiveReload} from '../utils'; 5 | 6 | export = function watchServe(gulp, plugins) { 7 | return function () { 8 | plugins.watch(join(APP_SRC, '**'), e => 9 | runSequence('build.dev', () => notifyLiveReload(e)) 10 | ); 11 | }; 12 | }; 13 | -------------------------------------------------------------------------------- /tools/tasks/watch.test.ts: -------------------------------------------------------------------------------- 1 | import {join} from 'path'; 2 | import {APP_SRC} from '../config'; 3 | 4 | export = function watchTest(gulp, plugins) { 5 | return function () { 6 | plugins.watch(join(APP_SRC, '**/*.ts'), () => gulp.start('build.test')); 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /tools/typings/connect-livereload.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'connect-livereload' { 2 | function connectLivereload(options?: any): any; 3 | module connectLivereload {} 4 | export = connectLivereload; 5 | } 6 | -------------------------------------------------------------------------------- /tools/typings/gulp-load-plugins.d.ts: -------------------------------------------------------------------------------- 1 | // Type definitions for gulp-load-plugins 2 | // Project: https://github.com/jackfranklin/gulp-load-plugins 3 | // Definitions by: Joe Skeen 4 | // Definitions: https://github.com/borisyankov/DefinitelyTyped 5 | 6 | // Does not support ES2015 import (import * as open from 'open'). 7 | 8 | /** Loads in any gulp plugins and attaches them to an object, freeing you up from having to manually require each gulp plugin. */ 9 | declare module 'gulp-load-plugins' { 10 | 11 | interface IOptions { 12 | /** the glob(s) to search for, default ['gulp-*', 'gulp.*'] */ 13 | pattern?: string[]; 14 | /** where to find the plugins, searched up from process.cwd(), default 'package.json' */ 15 | config?: string; 16 | /** which keys in the config to look within, default ['dependencies', 'devDependencies', 'peerDependencies'] */ 17 | scope?: string[]; 18 | /** what to remove from the name of the module when adding it to the context, default /^gulp(-|\.)/ */ 19 | replaceString?: RegExp; 20 | /** if true, transforms hyphenated plugin names to camel case, default true */ 21 | camelize?: boolean; 22 | /** whether the plugins should be lazy loaded on demand, default true */ 23 | lazy?: boolean; 24 | /** a mapping of plugins to rename, the key being the NPM name of the package, and the value being an alias you define */ 25 | rename?: IPluginNameMappings; 26 | } 27 | 28 | interface IPluginNameMappings { 29 | [npmPackageName: string]: string; 30 | } 31 | 32 | /** Loads in any gulp plugins and attaches them to an object, freeing you up from having to manually require each gulp plugin. */ 33 | function gulpLoadPlugins(options?: IOptions): T; 34 | module gulpLoadPlugins {} 35 | export = gulpLoadPlugins; 36 | } 37 | 38 | /** 39 | * Extend this interface to use Gulp plugins in your gulpfile.js 40 | */ 41 | interface IGulpPlugins { 42 | } 43 | -------------------------------------------------------------------------------- /tools/typings/karma.d.ts: -------------------------------------------------------------------------------- 1 | // Use this minimalistic definition file as bluebird dependency 2 | // generate a lot of errors. 3 | 4 | declare module 'karma' { 5 | var karma: IKarma; 6 | export = karma; 7 | interface IKarma { 8 | server: { 9 | start(options: any, callback: Function): void 10 | }; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tools/typings/merge-stream.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'merge-stream' { 2 | function mergeStream(...streams: NodeJS.ReadWriteStream[]): MergeStream; 3 | interface MergeStream extends NodeJS.ReadWriteStream { 4 | add(stream: NodeJS.ReadWriteStream): MergeStream; 5 | } 6 | module mergeStream {} 7 | export = mergeStream; 8 | } -------------------------------------------------------------------------------- /tools/typings/open.d.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/borisyankov/DefinitelyTyped/tree/master/open 2 | // Does not support ES2015 import (import * as open from 'open'). 3 | 4 | declare module 'open' { 5 | function open(target: string, app?: string): void; 6 | module open {} 7 | export = open; 8 | } 9 | -------------------------------------------------------------------------------- /tools/typings/run-sequence.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'run-sequence' { 2 | function runSequence(...args: any[]): void; 3 | module runSequence {} 4 | export = runSequence; 5 | } 6 | -------------------------------------------------------------------------------- /tools/typings/slash.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'slash' { 2 | function slash(path: string): string; 3 | module slash {} 4 | export = slash; 5 | } 6 | -------------------------------------------------------------------------------- /tools/typings/systemjs-builder.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'systemjs-builder' { 2 | class Builder { 3 | constructor(configObject?: any, baseUrl?: string, configPath?: string); 4 | bundle(source: string, target: string, options?: any): Promise; 5 | buildStatic(source: string, target: string, options?: any): Promise; 6 | } 7 | 8 | module Builder {} 9 | export = Builder; 10 | } 11 | -------------------------------------------------------------------------------- /tools/typings/tiny-lr.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'tiny-lr' { 2 | function tinylr(): ITinylr; 3 | module tinylr {} 4 | export = tinylr; 5 | 6 | interface ITinylr { 7 | changed(options: any): void; 8 | listen(port: number): void; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /tools/typings/yargs.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'yargs' { 2 | var yargs: IYargs; 3 | export = yargs; 4 | 5 | // Minimalistic but serves the usage in the seed. 6 | interface IYargs { 7 | argv: any; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /tools/utils.ts: -------------------------------------------------------------------------------- 1 | export * from './utils/template-injectables'; 2 | export * from './utils/template-locals'; 3 | export * from './utils/server'; 4 | export * from './utils/tasks_tools'; 5 | 6 | 7 | export function tsProjectFn(plugins) { 8 | return plugins.typescript.createProject('tsconfig.json', { 9 | typescript: require('typescript') 10 | }); 11 | } 12 | -------------------------------------------------------------------------------- /tools/utils/server.ts: -------------------------------------------------------------------------------- 1 | import * as connectLivereload from 'connect-livereload'; 2 | import * as express from 'express'; 3 | import * as tinylrFn from 'tiny-lr'; 4 | import * as openResource from 'open'; 5 | import * as serveStatic from 'serve-static'; 6 | import {resolve} from 'path'; 7 | import {APP_BASE, APP_DEST, DOCS_DEST, LIVE_RELOAD_PORT, DOCS_PORT, PORT} from '../config'; 8 | 9 | let tinylr = tinylrFn(); 10 | 11 | 12 | export function serveSPA() { 13 | let server = express(); 14 | tinylr.listen(LIVE_RELOAD_PORT); 15 | 16 | server.use( 17 | APP_BASE, 18 | connectLivereload({ port: LIVE_RELOAD_PORT }), 19 | express.static(process.cwd()) 20 | ); 21 | 22 | server.listen(PORT, () => 23 | openResource('http://localhost:' + PORT + APP_BASE + APP_DEST) 24 | ); 25 | } 26 | 27 | export function notifyLiveReload(e) { 28 | let fileName = e.path; 29 | tinylr.changed({ 30 | body: { files: [fileName] } 31 | }); 32 | } 33 | 34 | export function serveDocs() { 35 | let server = express(); 36 | 37 | server.use( 38 | APP_BASE, 39 | serveStatic(resolve(process.cwd(), DOCS_DEST)) 40 | ); 41 | 42 | server.listen(DOCS_PORT, () => 43 | openResource('http://localhost:' + DOCS_PORT + APP_BASE) 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /tools/utils/tasks_tools.ts: -------------------------------------------------------------------------------- 1 | import * as gulp from 'gulp'; 2 | import * as util from 'gulp-util'; 3 | import * as chalk from 'chalk'; 4 | import * as gulpLoadPlugins from 'gulp-load-plugins'; 5 | import * as _runSequence from 'run-sequence'; 6 | import {readdirSync, existsSync, lstatSync} from 'fs'; 7 | import {join} from 'path'; 8 | import {TOOLS_DIR} from '../config'; 9 | 10 | const TASKS_PATH = join(TOOLS_DIR, 'tasks'); 11 | 12 | // NOTE: Remove if no issues with runSequence function below. 13 | // export function loadTasks(): void { 14 | // scanDir(TASKS_PATH, (taskname) => registerTask(taskname)); 15 | // } 16 | 17 | export function task(taskname: string, option?: string) { 18 | util.log('Loading task', chalk.yellow(taskname, option || '')); 19 | return require(join('..', 'tasks', taskname))(gulp, gulpLoadPlugins(), option); 20 | } 21 | 22 | export function runSequence(...sequence: any[]) { 23 | let tasks = []; 24 | let _sequence = sequence.slice(0); 25 | sequence.pop(); 26 | 27 | scanDir(TASKS_PATH, taskname => tasks.push(taskname)); 28 | 29 | sequence.forEach(task => { 30 | if (tasks.indexOf(task) > -1) { registerTask(task); } 31 | }); 32 | 33 | return _runSequence(..._sequence); 34 | } 35 | 36 | // ---------- 37 | // Private. 38 | 39 | function registerTask(taskname: string, filename?: string, option: string = ''): void { 40 | gulp.task(taskname, task(filename || taskname, option)); 41 | } 42 | 43 | // TODO: add recursive lookup ? or enforce pattern file + folder (ie ./tools/utils & ./tools/utils.ts ) 44 | function scanDir(root: string, cb: (taskname: string) => void) { 45 | if (!existsSync(root)) return; 46 | 47 | walk(root); 48 | 49 | function walk(path) { 50 | let files = readdirSync(path); 51 | for (let i = 0; i < files.length; i += 1) { 52 | let file = files[i]; 53 | let curPath = join(path, file); 54 | // if (lstatSync(curPath).isDirectory()) { // recurse 55 | // path = file; 56 | // walk(curPath); 57 | // } 58 | if (lstatSync(curPath).isFile() && /\.ts$/.test(file)) { 59 | let taskname = file.replace(/(\.ts)/, ''); 60 | cb(taskname); 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /tools/utils/template-injectables.ts: -------------------------------------------------------------------------------- 1 | import * as slash from 'slash'; 2 | import {join} from 'path'; 3 | import {APP_BASE, APP_DEST, ENV} from '../config'; 4 | 5 | let injectables: string[] = []; 6 | 7 | export function injectableAssetsRef() { 8 | return injectables; 9 | } 10 | 11 | export function registerInjectableAssetsRef(paths: string[], target: string = '') { 12 | injectables = injectables.concat( 13 | paths 14 | .filter(path => !/(\.map)$/.test(path)) 15 | .map(path => join(target, slash(path).split('/').pop())) 16 | ); 17 | } 18 | 19 | export function transformPath(plugins, env) { 20 | return function (filepath) { 21 | filepath = ENV === 'prod' ? filepath.replace(`/${APP_DEST}`, '') : filepath; 22 | return slash(plugins.inject.transform.apply(plugins.inject.transform, arguments)); 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /tools/utils/template-locals.ts: -------------------------------------------------------------------------------- 1 | import {APP_BASE, APP_DEST, APP_ROOT, APP_TITLE, SYSTEM_CONFIG, VERSION} from '../config'; 2 | 3 | // TODO: Add an interface to register more template locals. 4 | export function templateLocals() { 5 | return { 6 | APP_BASE, 7 | APP_DEST, 8 | APP_ROOT, 9 | APP_TITLE, 10 | SYSTEM_CONFIG, 11 | VERSION 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "declaration": false, 6 | "noImplicitAny": false, 7 | "removeComments": true, 8 | "noLib": false, 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "sourceMap": true 12 | }, 13 | "exclude": [ 14 | "node_modules" 15 | ], 16 | "compileOnSave": false 17 | } 18 | -------------------------------------------------------------------------------- /tsd.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "v4", 3 | "repo": "DefinitelyTyped/DefinitelyTyped", 4 | "ref": "master", 5 | "path": "tools/typings/tsd", 6 | "bundle": "tools/typings/tsd/tsd.d.ts", 7 | "installed": { 8 | "systemjs/systemjs.d.ts": { 9 | "commit": "5c3e47967affa3c4128a3875d1664ba206ae1b0f" 10 | }, 11 | "gulp/gulp.d.ts": { 12 | "commit": "5c3e47967affa3c4128a3875d1664ba206ae1b0f" 13 | }, 14 | "q/Q.d.ts": { 15 | "commit": "5c3e47967affa3c4128a3875d1664ba206ae1b0f" 16 | }, 17 | "orchestrator/orchestrator.d.ts": { 18 | "commit": "5c3e47967affa3c4128a3875d1664ba206ae1b0f" 19 | }, 20 | "gulp-shell/gulp-shell.d.ts": { 21 | "commit": "5c3e47967affa3c4128a3875d1664ba206ae1b0f" 22 | }, 23 | "mime/mime.d.ts": { 24 | "commit": "5c3e47967affa3c4128a3875d1664ba206ae1b0f" 25 | }, 26 | "express/express.d.ts": { 27 | "commit": "5c3e47967affa3c4128a3875d1664ba206ae1b0f" 28 | }, 29 | "serve-static/serve-static.d.ts": { 30 | "commit": "5c3e47967affa3c4128a3875d1664ba206ae1b0f" 31 | }, 32 | "del/del.d.ts": { 33 | "commit": "5c3e47967affa3c4128a3875d1664ba206ae1b0f" 34 | }, 35 | "glob/glob.d.ts": { 36 | "commit": "5c3e47967affa3c4128a3875d1664ba206ae1b0f" 37 | }, 38 | "minimatch/minimatch.d.ts": { 39 | "commit": "5c3e47967affa3c4128a3875d1664ba206ae1b0f" 40 | }, 41 | "async/async.d.ts": { 42 | "commit": "5c3e47967affa3c4128a3875d1664ba206ae1b0f" 43 | }, 44 | "es6-promise/es6-promise.d.ts": { 45 | "commit": "923c5431d9447db9d5cf41adc5914e3c94c1ff10" 46 | }, 47 | "node/node.d.ts": { 48 | "commit": "5a8fc5ee71701431e4fdbb80c506e3c13f85a9ff" 49 | }, 50 | "gulp-util/gulp-util.d.ts": { 51 | "commit": "5a8fc5ee71701431e4fdbb80c506e3c13f85a9ff" 52 | }, 53 | "vinyl/vinyl.d.ts": { 54 | "commit": "5a8fc5ee71701431e4fdbb80c506e3c13f85a9ff" 55 | }, 56 | "through2/through2.d.ts": { 57 | "commit": "5a8fc5ee71701431e4fdbb80c506e3c13f85a9ff" 58 | }, 59 | "chalk/chalk.d.ts": { 60 | "commit": "5a8fc5ee71701431e4fdbb80c506e3c13f85a9ff" 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tsdrc: -------------------------------------------------------------------------------- 1 | { 2 | "strictSSL": false 3 | } 4 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "curly": false, 5 | "eofline": true, 6 | "indent": "spaces", 7 | "max-line-length": [true, 140], 8 | "member-ordering": [true, 9 | "public-before-private", 10 | "static-before-instance", 11 | "variables-before-functions" 12 | ], 13 | "no-arg": true, 14 | "no-construct": true, 15 | "no-duplicate-key": true, 16 | "no-duplicate-variable": true, 17 | "no-empty": true, 18 | "no-eval": true, 19 | "no-trailing-comma": true, 20 | "no-trailing-whitespace": true, 21 | "no-unused-expression": true, 22 | "no-unused-variable": true, 23 | "no-unreachable": true, 24 | "no-use-before-declare": true, 25 | "one-line": [true, 26 | "check-open-brace", 27 | "check-catch", 28 | "check-else", 29 | "check-whitespace" 30 | ], 31 | "quotemark": [true, "single"], 32 | "semicolon": true, 33 | "triple-equals": true, 34 | "variable-name": false 35 | } 36 | } 37 | --------------------------------------------------------------------------------