├── .babelrc ├── .codeclimate.yml ├── .csscomb.json ├── .csslintrc ├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .flowconfig ├── .gitattributes ├── .gitignore ├── .jscsrc ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── DockerfileMesos ├── LICENSE ├── Makefile ├── README.md ├── docs └── mesos-ui.gif ├── gulpfile.babel.js ├── marathon.json ├── package.json ├── src ├── actions │ └── ClusterActions.js ├── app.js ├── components │ ├── App │ │ ├── App.js │ │ └── package.json │ ├── Circle │ │ ├── Circle.js │ │ └── package.json │ ├── DashboardBox │ │ ├── DashboardBox.js │ │ └── package.json │ ├── Donut │ │ ├── Donut.js │ │ ├── Legend.js │ │ └── package.json │ ├── FrameworkBlock │ │ ├── FrameworkBlock.js │ │ └── package.json │ ├── Logo │ │ ├── Logo.js │ │ └── package.json │ ├── Navigation │ │ ├── Navigation.js │ │ └── package.json │ ├── PageTitle │ │ ├── PageTitle.js │ │ └── package.json │ ├── Sunburst │ │ ├── Legend.js │ │ ├── LegendItem.js │ │ ├── Sunburst.js │ │ └── package.json │ ├── TaskTable │ │ ├── TaskTable.js │ │ └── package.json │ ├── TaskVisualizer │ │ ├── TaskVisualizer.js │ │ ├── __tests__ │ │ │ ├── TaskVisualizer-test.js │ │ │ └── tasks-stub.json │ │ └── package.json │ └── ZookeeperRedirect │ │ ├── ZookeeperRedirect.js │ │ ├── __tests__ │ │ └── ZookeeperRedirect-test.js │ │ └── package.json ├── config │ └── config.js ├── constants │ └── ClusterConstants.js ├── core │ ├── Dispatcher.js │ └── __tests__ │ │ └── Dispatcher-test.js ├── decorators │ └── withContext.js ├── pages │ ├── Dashboard │ │ ├── Dashboard.js │ │ └── package.json │ ├── Frameworks │ │ ├── Frameworks.js │ │ └── package.json │ ├── Logs │ │ ├── Logs.js │ │ └── package.json │ ├── Nodes │ │ ├── Nodes.js │ │ └── package.json │ ├── NotFound │ │ ├── NotFound.js │ │ └── package.json │ └── Tasks │ │ ├── Tasks.js │ │ └── package.json ├── public │ ├── apple-touch-icon.png │ ├── assets │ │ ├── icon-framework-chronos.png │ │ ├── icon-framework-jenkins.png │ │ ├── icon-framework-marathon.png │ │ └── icon-framework-myriad.png │ ├── browserconfig.xml │ ├── crossdomain.xml │ ├── css │ │ ├── flexboxgrid.min.css │ │ ├── font-icons │ │ │ ├── material-icons │ │ │ │ ├── MaterialIcons-Regular.eot │ │ │ │ ├── MaterialIcons-Regular.ttf │ │ │ │ ├── MaterialIcons-Regular.woff │ │ │ │ ├── MaterialIcons-Regular.woff2 │ │ │ │ └── style.css │ │ │ └── material-ui-icons │ │ │ │ ├── icomoon.eot │ │ │ │ ├── icomoon.svg │ │ │ │ ├── icomoon.ttf │ │ │ │ ├── icomoon.woff │ │ │ │ ├── material-icons.woff2 │ │ │ │ ├── material-ui-icons.eot │ │ │ │ ├── material-ui-icons.svg │ │ │ │ ├── material-ui-icons.ttf │ │ │ │ ├── material-ui-icons.woff │ │ │ │ └── style.css │ │ ├── fonts │ │ │ └── roboto │ │ │ │ ├── Roboto-Light.ttf │ │ │ │ ├── Roboto-Medium.ttf │ │ │ │ ├── Roboto-Regular.ttf │ │ │ │ └── style.css │ │ └── main.css │ ├── favicon.ico │ ├── humans.txt │ ├── robots.txt │ ├── tile-wide.png │ └── tile.png ├── routes │ ├── api │ │ ├── mesos.js │ │ └── mesosFluxPropagator.js │ ├── dev.js │ ├── react-routes.js │ └── zookeeper.js ├── server.js ├── stores │ ├── ClusterStore.js │ ├── __tests__ │ │ └── ClusterStore-test.js │ └── schemes │ │ ├── frameworkScheme.js │ │ ├── nodeScheme.js │ │ └── statsScheme.js ├── stub.json └── templates │ └── index.html ├── webpack.config.js └── wercker.yml /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "stage": 0 3 | } -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | ratings: 3 | paths: 4 | - "src/**/*" 5 | - "**.js" 6 | engines: 7 | csslint: 8 | enabled: true 9 | checks: 10 | box-sizing: 11 | enabled: false 12 | eslint: 13 | enabled: true 14 | fixme: 15 | enabled: false 16 | 17 | exclude_paths: 18 | - src/public/css/flexboxgrid.min.css 19 | - src/public/css/fonts/roboto/style.css 20 | - src/public/css/font-icons/material-icons/style.css 21 | - src/public/css/font-icons/material-ui-icons/style.css 22 | -------------------------------------------------------------------------------- /.csscomb.json: -------------------------------------------------------------------------------- 1 | { 2 | "always-semicolon": true, 3 | "block-indent": 2, 4 | "color-case": "lower", 5 | "color-shorthand": true, 6 | "eof-newline": true, 7 | "leading-zero": false, 8 | "remove-empty-rulesets": true, 9 | "space-after-colon": 1, 10 | "space-after-combinator": 1, 11 | "space-before-selector-delimiter": 0, 12 | "space-between-declarations": "\n", 13 | "space-after-opening-brace": "\n", 14 | "space-before-closing-brace": "\n", 15 | "space-before-colon": 0, 16 | "space-before-combinator": 1, 17 | "space-before-opening-brace": 1, 18 | "strip-spaces": true, 19 | "unitless-zero": true, 20 | "vendor-prefix-align": true, 21 | "sort-order": [ 22 | [ 23 | "position", 24 | "top", 25 | "right", 26 | "bottom", 27 | "left", 28 | "z-index", 29 | "display", 30 | "float", 31 | "width", 32 | "min-width", 33 | "max-width", 34 | "height", 35 | "min-height", 36 | "max-height", 37 | "-webkit-box-sizing", 38 | "-moz-box-sizing", 39 | "box-sizing", 40 | "-webkit-appearance", 41 | "padding", 42 | "padding-top", 43 | "padding-right", 44 | "padding-bottom", 45 | "padding-left", 46 | "margin", 47 | "margin-top", 48 | "margin-right", 49 | "margin-bottom", 50 | "margin-left", 51 | "overflow", 52 | "overflow-x", 53 | "overflow-y", 54 | "-webkit-overflow-scrolling", 55 | "-ms-overflow-x", 56 | "-ms-overflow-y", 57 | "-ms-overflow-style", 58 | "clip", 59 | "clear", 60 | "font", 61 | "font-family", 62 | "font-size", 63 | "font-style", 64 | "font-weight", 65 | "font-variant", 66 | "font-size-adjust", 67 | "font-stretch", 68 | "font-effect", 69 | "font-emphasize", 70 | "font-emphasize-position", 71 | "font-emphasize-style", 72 | "font-smooth", 73 | "-webkit-hyphens", 74 | "-moz-hyphens", 75 | "hyphens", 76 | "line-height", 77 | "color", 78 | "text-align", 79 | "-webkit-text-align-last", 80 | "-moz-text-align-last", 81 | "-ms-text-align-last", 82 | "text-align-last", 83 | "text-emphasis", 84 | "text-emphasis-color", 85 | "text-emphasis-style", 86 | "text-emphasis-position", 87 | "text-decoration", 88 | "text-indent", 89 | "text-justify", 90 | "text-outline", 91 | "-ms-text-overflow", 92 | "text-overflow", 93 | "text-overflow-ellipsis", 94 | "text-overflow-mode", 95 | "text-shadow", 96 | "text-transform", 97 | "text-wrap", 98 | "-webkit-text-size-adjust", 99 | "-ms-text-size-adjust", 100 | "letter-spacing", 101 | "-ms-word-break", 102 | "word-break", 103 | "word-spacing", 104 | "-ms-word-wrap", 105 | "word-wrap", 106 | "-moz-tab-size", 107 | "-o-tab-size", 108 | "tab-size", 109 | "white-space", 110 | "vertical-align", 111 | "list-style", 112 | "list-style-position", 113 | "list-style-type", 114 | "list-style-image", 115 | "pointer-events", 116 | "-ms-touch-action", 117 | "touch-action", 118 | "cursor", 119 | "visibility", 120 | "zoom", 121 | "flex-direction", 122 | "flex-order", 123 | "flex-pack", 124 | "flex-align", 125 | "table-layout", 126 | "empty-cells", 127 | "caption-side", 128 | "border-spacing", 129 | "border-collapse", 130 | "content", 131 | "quotes", 132 | "counter-reset", 133 | "counter-increment", 134 | "resize", 135 | "-webkit-user-select", 136 | "-moz-user-select", 137 | "-ms-user-select", 138 | "-o-user-select", 139 | "user-select", 140 | "nav-index", 141 | "nav-up", 142 | "nav-right", 143 | "nav-down", 144 | "nav-left", 145 | "background", 146 | "background-color", 147 | "background-image", 148 | "-ms-filter:\\'progid:DXImageTransform.Microsoft.gradient", 149 | "filter:progid:DXImageTransform.Microsoft.gradient", 150 | "filter:progid:DXImageTransform.Microsoft.AlphaImageLoader", 151 | "filter", 152 | "background-repeat", 153 | "background-attachment", 154 | "background-position", 155 | "background-position-x", 156 | "background-position-y", 157 | "-webkit-background-clip", 158 | "-moz-background-clip", 159 | "background-clip", 160 | "background-origin", 161 | "-webkit-background-size", 162 | "-moz-background-size", 163 | "-o-background-size", 164 | "background-size", 165 | "border", 166 | "border-color", 167 | "border-style", 168 | "border-width", 169 | "border-top", 170 | "border-top-color", 171 | "border-top-style", 172 | "border-top-width", 173 | "border-right", 174 | "border-right-color", 175 | "border-right-style", 176 | "border-right-width", 177 | "border-bottom", 178 | "border-bottom-color", 179 | "border-bottom-style", 180 | "border-bottom-width", 181 | "border-left", 182 | "border-left-color", 183 | "border-left-style", 184 | "border-left-width", 185 | "border-radius", 186 | "border-top-left-radius", 187 | "border-top-right-radius", 188 | "border-bottom-right-radius", 189 | "border-bottom-left-radius", 190 | "-webkit-border-image", 191 | "-moz-border-image", 192 | "-o-border-image", 193 | "border-image", 194 | "-webkit-border-image-source", 195 | "-moz-border-image-source", 196 | "-o-border-image-source", 197 | "border-image-source", 198 | "-webkit-border-image-slice", 199 | "-moz-border-image-slice", 200 | "-o-border-image-slice", 201 | "border-image-slice", 202 | "-webkit-border-image-width", 203 | "-moz-border-image-width", 204 | "-o-border-image-width", 205 | "border-image-width", 206 | "-webkit-border-image-outset", 207 | "-moz-border-image-outset", 208 | "-o-border-image-outset", 209 | "border-image-outset", 210 | "-webkit-border-image-repeat", 211 | "-moz-border-image-repeat", 212 | "-o-border-image-repeat", 213 | "border-image-repeat", 214 | "outline", 215 | "outline-width", 216 | "outline-style", 217 | "outline-color", 218 | "outline-offset", 219 | "-webkit-box-shadow", 220 | "-moz-box-shadow", 221 | "box-shadow", 222 | "filter:progid:DXImageTransform.Microsoft.Alpha(Opacity", 223 | "-ms-filter:\\'progid:DXImageTransform.Microsoft.Alpha", 224 | "opacity", 225 | "-ms-interpolation-mode", 226 | "-webkit-transition", 227 | "-moz-transition", 228 | "-ms-transition", 229 | "-o-transition", 230 | "transition", 231 | "-webkit-transition-delay", 232 | "-moz-transition-delay", 233 | "-ms-transition-delay", 234 | "-o-transition-delay", 235 | "transition-delay", 236 | "-webkit-transition-timing-function", 237 | "-moz-transition-timing-function", 238 | "-ms-transition-timing-function", 239 | "-o-transition-timing-function", 240 | "transition-timing-function", 241 | "-webkit-transition-duration", 242 | "-moz-transition-duration", 243 | "-ms-transition-duration", 244 | "-o-transition-duration", 245 | "transition-duration", 246 | "-webkit-transition-property", 247 | "-moz-transition-property", 248 | "-ms-transition-property", 249 | "-o-transition-property", 250 | "transition-property", 251 | "-webkit-transform", 252 | "-moz-transform", 253 | "-ms-transform", 254 | "-o-transform", 255 | "transform", 256 | "-webkit-transform-origin", 257 | "-moz-transform-origin", 258 | "-ms-transform-origin", 259 | "-o-transform-origin", 260 | "transform-origin", 261 | "-webkit-animation", 262 | "-moz-animation", 263 | "-ms-animation", 264 | "-o-animation", 265 | "animation", 266 | "-webkit-animation-name", 267 | "-moz-animation-name", 268 | "-ms-animation-name", 269 | "-o-animation-name", 270 | "animation-name", 271 | "-webkit-animation-duration", 272 | "-moz-animation-duration", 273 | "-ms-animation-duration", 274 | "-o-animation-duration", 275 | "animation-duration", 276 | "-webkit-animation-play-state", 277 | "-moz-animation-play-state", 278 | "-ms-animation-play-state", 279 | "-o-animation-play-state", 280 | "animation-play-state", 281 | "-webkit-animation-timing-function", 282 | "-moz-animation-timing-function", 283 | "-ms-animation-timing-function", 284 | "-o-animation-timing-function", 285 | "animation-timing-function", 286 | "-webkit-animation-delay", 287 | "-moz-animation-delay", 288 | "-ms-animation-delay", 289 | "-o-animation-delay", 290 | "animation-delay", 291 | "-webkit-animation-iteration-count", 292 | "-moz-animation-iteration-count", 293 | "-ms-animation-iteration-count", 294 | "-o-animation-iteration-count", 295 | "animation-iteration-count", 296 | "-webkit-animation-direction", 297 | "-moz-animation-direction", 298 | "-ms-animation-direction", 299 | "-o-animation-direction", 300 | "animation-direction" 301 | ] 302 | ] 303 | } 304 | -------------------------------------------------------------------------------- /.csslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "adjoining-classes": false, 3 | "box-sizing": false, 4 | "box-model": false, 5 | "compatible-vendor-prefixes": false, 6 | "floats": false, 7 | "font-sizes": false, 8 | "gradients": false, 9 | "important": false, 10 | "known-properties": false, 11 | "outline-none": false, 12 | "qualified-headings": false, 13 | "regex-selectors": false, 14 | "shorthand": false, 15 | "text-indent": false, 16 | "unique-headings": false, 17 | "universal-selector": false, 18 | "unqualified-attributes": false 19 | } 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # http://editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | # Change these settings to your own preference 10 | indent_style = space 11 | indent_size = 2 12 | 13 | # We recommend you to keep these unchanged 14 | end_of_line = lf 15 | charset = utf-8 16 | trim_trailing_whitespace = true 17 | insert_final_newline = true 18 | 19 | [*.md] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # Ignore jest unit test files 2 | **/__tests__/* 3 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "plugins": [ 4 | "react" 5 | ], 6 | "env": { 7 | "browser": true, 8 | "node": true, 9 | "es6": true 10 | }, 11 | "globals": { 12 | "__DEV__": true, 13 | "__SERVER__": true, 14 | "window": true, 15 | "document": true 16 | }, 17 | "ecmaFeatures": { 18 | "jsx": true 19 | }, 20 | "rules": { 21 | // Strict mode 22 | "strict": [0, "global"], 23 | 24 | // Code style 25 | "indent": [2, 2, {SwitchCase: 1}], 26 | "jsx-quotes": 1, 27 | "quotes": [2, "single"], 28 | "camelcase": 0, 29 | "no-undefined": 2, 30 | "no-unused-vars": 0, 31 | "curly": 2, 32 | "no-trailing-spaces": 2, 33 | 34 | // React 35 | "react/display-name": 0, 36 | "react/jsx-boolean-value": 0, 37 | "react/jsx-no-undef": 1, 38 | "react/jsx-sort-prop-types": 0, 39 | "react/jsx-sort-props": 0, 40 | "react/jsx-uses-react": 1, 41 | "react/jsx-uses-vars": 1, 42 | "react/no-did-mount-set-state": 1, 43 | "react/no-did-update-set-state": 1, 44 | "react/no-multi-comp": 1, 45 | "react/no-unknown-property": 1, 46 | "react/prop-types": 1, 47 | "react/react-in-jsx-scope": 1, 48 | "react/self-closing-comp": 1, 49 | "react/sort-comp": 1, 50 | "react/wrap-multilines": 1 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/build 3 | .*/config 4 | .*/node_modules 5 | .*/gulpfile.js 6 | 7 | [include] 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Automatically normalize line endings for all text-based files 2 | # http://git-scm.com/docs/gitattributes#_end_of_line_conversion 3 | * text=auto 4 | 5 | # For the following file types, normalize line endings to LF on 6 | # checkin and prevent conversion to CRLF when they are checked out 7 | # (this is required in order to prevent newline related issues like, 8 | # for example, after the build script is run) 9 | .* text eol=lf 10 | *.css text eol=lf 11 | *.html text eol=lf 12 | *.jade text eol=lf 13 | *.js text eol=lf 14 | *.json text eol=lf 15 | *.md text eol=lf 16 | *.sh text eol=lf 17 | *.txt text eol=lf 18 | *.xml text eol=lf 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Include your project-specific ignores in this file 2 | # Read about how to use .gitignore: https://help.github.com/articles/ignoring-files 3 | 4 | build 5 | node_modules 6 | npm-debug.log 7 | coverage 8 | TempDockerfile 9 | 10 | # Local files. 11 | .DS_Store 12 | .idea 13 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "google", 3 | "disallowSpacesInAnonymousFunctionExpression": null, 4 | "validateLineBreaks": "LF", 5 | "validateIndentation": 2, 6 | "excludeFiles": ["build/**", "node_modules/**"], 7 | "esprima": "esprima-fb" 8 | } 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.1.4 (WIP) 2 | 3 | * This version provides support for Zookeeper Leader Election when running standalone mode. 4 | 5 | ## 0.1.3 (March 9, 2016) 6 | 7 | * This version provides tasks support with both table and visualizer format. 8 | 9 | ## 0.1.2 (Dec 7, 2015) 10 | 11 | * This version removes the dependency with the nodejs server. 12 | * This version provides a ready to be served by mesos-core UI. 13 | * This version provides zookeeper leader redirect functionality. 14 | * This version **deprecates the standalone previews mode** in order to provide deeper and easy integration with mesos core. 15 | 16 | ## 0.1.1 (Oct 21, 2015) 17 | 18 | * This version removes any dependency with external static files. 19 | * This version includes Apache Myriad framework logo. 20 | 21 | ## 0.1.0 (Oct 07, 2015) 22 | 23 | * Initial release 24 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | How to contribute 2 | ================= 3 | 4 | [Mesos-ui](https://github.com/Capgemini/mesos-ui) is based on an strong open-source philosophy so we would love your contributions to make it better. 5 | 6 | 7 | ## Code of Conduct 8 | 9 | This project adheres to the [Open Code of Conduct](http://todogroup.org/opencodeofconduct/#mesos-ui/digitaldevops.uk@capgemini.com). By participating, you are expected to uphold this code. 10 | 11 | ## Mesos-ui Core 12 | 13 | * Mesos-ui an alternative web UI for [Apache Mesos](http://mesos.apache.org/) which provides a non-opinionated rich user experience for managing and monitoring your Mesos Cluster. 14 | 15 | * Mesos-ui does not make any assumptions so we'd love you to provide your addons specific features on top of it that can be resued across the community. 16 | 17 | * We use [ReactJS](https://facebook.github.io/react/index.html) so it leverages encapsulation, code reusability and separation of concerns via React Components. 18 | 19 | * We use [material-ui](http://material-ui.com/) which combines quality of [Google Design](https://design.google.com) with power of React. 20 | 21 | 22 | ## Contributing to Mesos-ui 23 | 24 | * Submit an issue describing your proposed change to the Mesos-ui repo. 25 | 26 | * Fork the repo, develop and test your feature. 27 | 28 | * Submit a pull request. 29 | 30 | ### New Widgets. 31 | 32 | * Mesos-ui is composed of reusable bits. For creating a new component please read about [React Reusable Components](https://facebook.github.io/react/docs/why-react.html) and [ES6 Classes.](https://facebook.github.io/react/docs/reusable-components.html#es6-classes) 33 | 34 | * Individual components live in ```src/components/your-component``` 35 | 36 | ### New Themes. 37 | 38 | * See http://material-ui.com/#/customization/themes 39 | 40 | * New themes should live in ```src/themes``` 41 | 42 | * A new theme should be purely a JS file defining your specific styles for rendering the material-ui components. 43 | 44 | ``` 45 | checkbox: { 46 | boxColor: palette.textColor, 47 | checkedColor: palette.primary1Color, 48 | requiredColor: palette.primary1Color, 49 | disabledColor: palette.disabledColor, 50 | labelColor: palette.textColor, 51 | labelDisabledColor: palette.disabledColor 52 | } 53 | ``` 54 | 55 | * Components should be theme agnostic, being injected as a context. 56 | 57 | ``` 58 | static contextTypes = { 59 | muiTheme: React.PropTypes.object 60 | }; 61 | 62 | getStyles() { 63 | let palette = this.context.muiTheme.palette; 64 | computer: { 65 | color: palette.primary3Color, 66 | ``` 67 | 68 | ### Replacing the Mesos default UI 69 | 70 | #### With Docker 71 | 72 | You can use [capgemini/mesos-ui](https://hub.docker.com/r/capgemini/mesos-ui/) for running a container extending [mesosphere/mesos-master](https://hub.docker.com/r/mesosphere/mesos-master/) with this mesos-ui as the default Mesos UI. 73 | 74 | ##### Building the Docker image 75 | 76 | ``` 77 | git clone https://github.com/Capgemini/mesos-ui.git mesos-ui 78 | cd mesos-ui 79 | npm install 80 | 81 | export MESOS_VERSION="Mesos version to build from" 82 | //This must be an existing local git tag. 83 | export MESOS_UI_VERSION="Mesos ui version" 84 | 85 | make 86 | ``` 87 | 88 | #### Without Docker 89 | 90 | ``` 91 | git clone https://github.com/Capgemini/mesos-ui.git mesos-ui 92 | cd mesos-ui 93 | npm install 94 | gulp build 95 | ``` 96 | 97 | Run mesos like: 98 | 99 | ```./bin/mesos-master.sh --ip=127.0.0.1 --work_dir=/var/lib/mesos --log_dir=/var/lib/mesos/logs --webui_dir=/path-to/mesos-ui/build/``` 100 | 101 | or using environment variables: 102 | 103 | ```export MESOS_WEBUI_DIR=/your-path/mesos-ui/build/``` 104 | 105 | See http://mesos.apache.org/gettingstarted/ 106 | 107 | See http://mesos.apache.org/documentation/latest/configuration/ 108 | 109 | ## Compatibility 110 | 111 | This code has been tested against Mesos version 0.23 at the time of writing and all subsequent versions until 0.27 included. 112 | 113 | ## Developing locally 114 | 115 | ### Prerequisites 116 | 117 | NodeJS (+ NPM) version 4.x. See [https://nodejs.org/en/download/releases/](https://nodejs.org/en/download/releases/) for installation instructions. 118 | 119 | Install gulp package for global use: 120 | 121 | ``` 122 | sudo npm install -g gulp 123 | ``` 124 | 125 | To run the app, first clone the repo: 126 | 127 | ``` 128 | git clone https://github.com/Capgemini/mesos-ui.git mesos-ui 129 | ``` 130 | 131 | Install the NPM packages: 132 | 133 | ``` 134 | cd mesos-ui 135 | npm install 136 | ``` 137 | Make sure you export the environment variable MESOS_ENDPOINT to point to the stub server. 138 | ```export MESOS_ENDPOINT=http://127.0.0.1:8000``` 139 | 140 | Serve the app 141 | 142 | ``` 143 | gulp 144 | ``` 145 | 146 | At this point the app should open in the browser the page `http://localhost:3000`. 147 | 148 | The application is using a stub JSON server to mock the Mesos APIs 149 | so you don't necessarily need a working Mesos Cluster. For that we are using 150 | [json-server](https://github.com/typicode/json-server). 151 | 152 | The application should be available on http://localhost:5000. 153 | 154 | The stub data is at [src/stub.json](https://github.com/Capgemini/mesos-ui/blob/master/src/stub.json). The UI for json-server should be available on http://localhost:8000 155 | 156 | For using the previews standalone version of mesos-ui [check this out](https://github.com/Capgemini/mesos-ui/tree/0.1.1) 157 | 158 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mhart/alpine-node:6.5 2 | 3 | MAINTAINER Graham Taylor 4 | 5 | RUN apk add --update make gcc g++ python 6 | RUN npm install -g gulp 7 | 8 | WORKDIR /src 9 | ADD . . 10 | 11 | RUN npm install 12 | 13 | EXPOSE 5000 14 | EXPOSE 8000 15 | CMD ["gulp", "serve"] 16 | -------------------------------------------------------------------------------- /DockerfileMesos: -------------------------------------------------------------------------------- 1 | FROM mesosphere/mesos-master:VERSION 2 | MAINTAINER alberto.garcial@hotmail.com 3 | COPY build/master /web-ui/master 4 | ENV MESOS_WEBUI_DIR /web-ui 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2015 Capgemini 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all build_ui_snapshot build_standalone_image 2 | 3 | MESOS_VERSION ?= latest 4 | 5 | MESOS_UI_VERSION ?= latest 6 | 7 | all: build_mesos_image build_standalone_image 8 | 9 | build_ui_snapshot: 10 | git checkout tags/$(MESOS_UI_VERSION) 11 | gulp build 12 | 13 | build_mesos_image: build_ui_snapshot 14 | sed "s/VERSION/$(MESOS_VERSION)/g" DockerfileMesos > TempDockerfile 15 | docker build -f TempDockerfile -t capgemini/mesos-ui:$(MESOS_UI_VERSION) -t capgemini/mesos-ui:latest . 16 | 17 | build_standalone_image: 18 | docker build -t capgemini/mesos-ui:standalone-$(MESOS_UI_VERSION) -t capgemini/mesos-ui:standalone-latest . 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mesos UI 2 | 3 | [![Join the chat at https://gitter.im/Capgemini/mesos-ui](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Capgemini/mesos-ui?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | 5 | [![wercker status](https://app.wercker.com/status/3e566621ba967bfeb6ee57a76ddf42cc/s/master "wercker status")](https://app.wercker.com/project/bykey/3e566621ba967bfeb6ee57a76ddf42cc) 6 | [![Code Climate](https://codeclimate.com/github/Capgemini/mesos-ui/badges/gpa.svg)](https://codeclimate.com/github/Capgemini/mesos-ui) 7 | [![Test Coverage](https://codeclimate.com/github/Capgemini/mesos-ui/badges/coverage.svg)](https://codeclimate.com/github/Capgemini/mesos-ui/coverage) 8 | 9 | A responsive, realtime dashboard for Apache Mesos built using Node.js, React.js. 10 | 11 | ![dashboard](docs/mesos-ui.gif) 12 | 13 | ## Usage as standalone while keeping the default Mesos UI. 14 | 15 | You can run and deploy this app in standalone mode via docker like: 16 | 17 | ```docker run -p 5000:5000 -e ZOOKEEPER_ADDRESS="ip1:2181,ip2:2181,ip3:2181" capgemini/mesos-ui:standalone-$TAG``` 18 | 19 | or using marathon: 20 | 21 | replace ZOOKEEPER_ADDRESS with the address of your zookeeper instances and run: 22 | 23 | ``` curl -X POST -HContent-Type:application/json -d @marathon.json http://MARATHON_ENDPOINT:8080/v2/apps ``` 24 | 25 | ## Usage as a replacement for the default Mesos UI. 26 | 27 | The quickest way to check it out is just run: 28 | 29 | ``` docker run --net=host -e MESOS_LOG_DIR=/logs capgemini/mesos-ui:latest ``` 30 | 31 | And you should be able to access to 127.0.0.1:5050 via browser. 32 | 33 | ## Powered By Mesos UI 34 | Organisations using Mesos UI. If your company uses Mesos UI, we would be glad to get your feedback :) 35 | 36 | - [Capgemini](https://www.uk.capgemini.com) 37 | - [Gumtree AU - Ebay Classifieds group](https://www.gumtree.com.au) 38 | - [今日头条 - Toutiao](http://app.toutiao.com/news_article/) 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /docs/mesos-ui.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Capgemini/mesos-ui/b45798230528ac176820661696a770bf97903a7a/docs/mesos-ui.gif -------------------------------------------------------------------------------- /gulpfile.babel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import path from 'path'; 4 | import cp from 'child_process'; 5 | import gulp from 'gulp'; 6 | import gulpLoadPlugins from 'gulp-load-plugins'; 7 | import del from 'del'; 8 | import mkdirp from 'mkdirp'; 9 | import runSequence from 'run-sequence'; 10 | import webpack from 'webpack'; 11 | import minimist from 'minimist'; 12 | import eslint from 'gulp-eslint'; 13 | 14 | const $ = gulpLoadPlugins(); 15 | const argv = minimist(process.argv.slice(2)); 16 | const src = Object.create(null); 17 | 18 | let watch = false; 19 | let browserSync; 20 | 21 | // The default task 22 | gulp.task('default', ['sync']); 23 | 24 | // Clean output directory 25 | gulp.task('clean', cb => { 26 | del(['.tmp', 'build/*', '!build/.git'], {dot: true}, () => { 27 | mkdirp('build/master/static', cb); 28 | }); 29 | }); 30 | 31 | gulp.task('lint', function () { 32 | return gulp.src(['src/**/*.js', 'gulpfile.babel.js', 'webpack.config.js']) 33 | // eslint() attaches the lint output to the eslint property 34 | // of the file object so it can be used by other modules. 35 | .pipe(eslint()) 36 | // eslint.format() outputs the lint results to the console. 37 | // Alternatively use eslint.formatEach() (see Docs). 38 | .pipe(eslint.format()) 39 | // To have the process exit with an error code (1) on 40 | // lint error, return the stream and pipe to failAfterError last. 41 | .pipe(eslint.failAfterError()); 42 | }); 43 | 44 | // Static files 45 | gulp.task('assets', () => { 46 | src.assets = 'src/public/**'; 47 | return gulp.src(src.assets) 48 | .pipe($.changed('build/master/static')) 49 | .pipe(gulp.dest('build/master/static')) 50 | .pipe($.size({title: 'assets'})); 51 | }); 52 | 53 | // Resource files 54 | gulp.task('resources', () => { 55 | src.resources = [ 56 | 'package.json', 57 | 'src/templates/**' 58 | ]; 59 | return gulp.src(src.resources) 60 | .pipe($.changed('build/master/static')) 61 | .pipe(gulp.dest('build/master/static')) 62 | .pipe($.size({title: 'resources'})); 63 | }); 64 | 65 | // Bundle 66 | gulp.task('bundle', cb => { 67 | const config = require('./webpack.config.js'); 68 | const bundler = webpack(config); 69 | const verbose = !!argv.verbose; 70 | let bundlerRunCount = 0; 71 | 72 | function bundle(err, stats) { 73 | if (err) { 74 | throw new $.util.PluginError('webpack', err); 75 | } 76 | 77 | console.log(stats.toString({ 78 | colors: $.util.colors.supportsColor, 79 | hash: verbose, 80 | version: verbose, 81 | timings: verbose, 82 | chunks: verbose, 83 | chunkModules: verbose, 84 | cached: verbose, 85 | cachedAssets: verbose 86 | })); 87 | 88 | if (++bundlerRunCount === (watch ? config.length : 1)) { 89 | return cb(); 90 | } 91 | } 92 | 93 | if (watch) { 94 | bundler.watch(200, bundle); 95 | } else { 96 | bundler.run(bundle); 97 | } 98 | }); 99 | 100 | // Build the app from source code 101 | gulp.task('build', cb => { 102 | runSequence(['assets', 'resources'], ['bundle'], cb); 103 | }); 104 | 105 | // Build and start watching for modifications 106 | gulp.task('build:watch', cb => { 107 | watch = true; 108 | runSequence('build', () => { 109 | gulp.watch(src.assets, ['assets']); 110 | gulp.watch(src.resources, ['resources']); 111 | cb(); 112 | }); 113 | }); 114 | 115 | // Launch a Node.js/Express server 116 | gulp.task('serve', ['build:watch'], cb => { 117 | src.server = [ 118 | 'build/server.js', 119 | 'build/master/static/**/*' 120 | ]; 121 | let started = false; 122 | let server = (function startup() { 123 | const child = cp.fork('build/server.js', { 124 | env: Object.assign({NODE_ENV: 'development'}, process.env) 125 | }); 126 | child.once('message', message => { 127 | if (message.match(/^online$/)) { 128 | if (browserSync) { 129 | browserSync.reload(); 130 | } 131 | if (!started) { 132 | started = true; 133 | gulp.watch(src.server, function() { 134 | $.util.log('Restarting development server.'); 135 | server.kill('SIGTERM'); 136 | server = startup(); 137 | }); 138 | cb(); 139 | } 140 | } 141 | }); 142 | return child; 143 | })(); 144 | 145 | process.on('exit', () => server.kill('SIGTERM')); 146 | }); 147 | 148 | // Launch BrowserSync development server 149 | gulp.task('sync', ['serve'], cb => { 150 | browserSync = require('browser-sync'); 151 | 152 | browserSync({ 153 | logPrefix: 'RSK', 154 | notify: false, 155 | // Run as an https by setting 'https: true' 156 | // Note: this uses an unsigned certificate which on first access 157 | // will present a certificate warning in the browser. 158 | https: false, 159 | // Informs browser-sync to proxy our Express app which would run 160 | // at the following location 161 | proxy: 'localhost:5000' 162 | }, cb); 163 | 164 | process.on('exit', () => browserSync.exit()); 165 | 166 | gulp.watch(['build/**/*.*'].concat( 167 | src.server.map(file => '!' + file) 168 | ), file => { 169 | browserSync.reload(path.relative(__dirname, file.path)); 170 | }); 171 | }); 172 | -------------------------------------------------------------------------------- /marathon.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "id": "mesos-ui", 4 | "instances": 1, 5 | "cpus": 1, 6 | "mem": 512, 7 | "env": { 8 | "ZOOKEEPER_ADDRESS": "127.0.0.1:2181" 9 | }, 10 | "labels": { 11 | "DCOS_PACKAGE_NAME": "mesos-ui", 12 | "DCOS_PACKAGE_IS_FRAMEWORK": "false", 13 | "DCOS_PACKAGE_VERSION": "0.1.0" 14 | }, 15 | "healthChecks": [ 16 | { 17 | "gracePeriodSeconds": 120, 18 | "intervalSeconds": 15, 19 | "maxConsecutiveFailures": 10, 20 | "path": "/", 21 | "portIndex": 0, 22 | "protocol": "HTTP", 23 | "timeoutSeconds": 5 24 | } 25 | ], 26 | "container": { 27 | "type": "DOCKER", 28 | "docker": { 29 | "image": "capgemini/mesos-ui:standalone-latest", 30 | "network": "BRIDGE", 31 | "portMappings": [ 32 | { 33 | "containerPort": 0, 34 | "hostPort": 0, 35 | "servicePort": 5000, 36 | "protocol": "tcp" 37 | } 38 | ] 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mesos-ui", 3 | "version": "0.1.0", 4 | "description": "A UI for Mesos built on React.JS.", 5 | "license": "MIT", 6 | "bugs": { 7 | "url": "https://github.com/capgemini/mesos-ui/issues" 8 | }, 9 | "homepage": "https://github.com/capgemini/mesos-ui", 10 | "keywords": [ 11 | "mesos", 12 | "apache", 13 | "ui", 14 | "react" 15 | ], 16 | "private": true, 17 | "engines": { 18 | "node": ">= 4.1.2", 19 | "npm": ">= 2.14" 20 | }, 21 | "dependencies": { 22 | "node-zookeeper-client": "0.2.2", 23 | "array-find": "~1.0.0", 24 | "babel": "5.8.23", 25 | "babel-plugin-flow-comments": "~1.0.9", 26 | "classnames": "2.1.3", 27 | "d3": "~3.5.6", 28 | "eventemitter3": "1.1.0", 29 | "exenv": "~1.2.0", 30 | "express": "4.13.3", 31 | "fastclick": "1.0.6", 32 | "flux": "2.1.1", 33 | "is-plain-object": "~2.0.1", 34 | "lodash": "3.10.1", 35 | "material-ui": "0.11.1", 36 | "node-sass": "~3.3.0", 37 | "normalize.css": "3.0.3", 38 | "object-assign": "~4.0.1", 39 | "radium": "~0.14.1", 40 | "react": "0.13.3", 41 | "react-motion": "^0.3.0", 42 | "react-router": "0.13.3", 43 | "react-tap-event-plugin": "0.1.7", 44 | "sass-loader": "~1.0.3", 45 | "source-map-support": "0.3.2", 46 | "superagent": "1.4.0", 47 | "history": "1.12.0", 48 | "envify": "3.4.0", 49 | "express-http-proxy": "0.6.0", 50 | "transform-loader": "0.2.3" 51 | }, 52 | "devDependencies": { 53 | "autoprefixer-core": "~5.2.0", 54 | "babel-core": "~5.8.25", 55 | "babel-eslint": "~4.1.7", 56 | "babel-loader": "~5.3.2", 57 | "browser-sync": "~2.9.6", 58 | "css-loader": "~0.18.0", 59 | "csscomb": "~3.1.8", 60 | "del": "~2.0.2", 61 | "eslint": "~1.5.0", 62 | "eslint-loader": "~1.0.0", 63 | "eslint-plugin-react": "~3.0.0", 64 | "gulp": "~3.9.0", 65 | "gulp-changed": "~1.3.0", 66 | "gulp-if": "~1.2.5", 67 | "gulp-load-plugins": "~0.10.0", 68 | "gulp-eslint": "1.0.0", 69 | "gulp-jshint": "1.11.2", 70 | "jshint-stylish": "2.0.1", 71 | "gulp-rename": "~1.2.2", 72 | "gulp-size": "~2.0.0", 73 | "gulp-util": "~3.0.6", 74 | "jest-cli": "0.5.7", 75 | "babel-jest": "5.3.0", 76 | "json-server": "~0.8.0", 77 | "minimist": "~1.2.0", 78 | "mkdirp": "^0.5.1", 79 | "postcss": "~5.0.6", 80 | "postcss-loader": "~0.6.0", 81 | "psi": "~1.0.6", 82 | "run-sequence": "~1.1.3", 83 | "style-loader": "~0.12.4", 84 | "url-loader": "~0.5.6", 85 | "webpack": "~1.12.2" 86 | }, 87 | "jest": { 88 | "rootDir": "./src", 89 | "collectCoverage": true, 90 | "scriptPreprocessor": "../node_modules/babel-jest", 91 | "unmockedModulePathPatterns": [ 92 | "react" 93 | ] 94 | }, 95 | "scripts": { 96 | "start": "node ./build/server.js", 97 | "lint": "gulp lint && csscomb src/components --lint", 98 | "comb": "csscomb src/components --verbose", 99 | "test": "eslint ./src && jest", 100 | "coverage": "jest --coverage", 101 | "build": "gulp build", 102 | "serve": "gulp serve", 103 | "sync": "gulp sync" 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/actions/ClusterActions.js: -------------------------------------------------------------------------------- 1 | import Dispatcher from '../core/Dispatcher'; 2 | import ClusterConstants from '../constants/ClusterConstants'; 3 | 4 | export default { 5 | 6 | refreshStats(stats) { 7 | Dispatcher.dispatch({ 8 | actionType: ClusterConstants.CLUSTER_REFRESH_STATS, 9 | stats: stats 10 | }); 11 | }, 12 | refreshLogs(logs) { 13 | Dispatcher.dispatch({ 14 | actionType: ClusterConstants.CLUSTER_REFRESH_LOGS, 15 | logs: logs 16 | }); 17 | }, 18 | refreshState(state) { 19 | Dispatcher.dispatch({ 20 | actionType: ClusterConstants.CLUSTER_REFRESH_STATE, 21 | state: state 22 | }); 23 | } 24 | 25 | }; 26 | -------------------------------------------------------------------------------- /src/app.js: -------------------------------------------------------------------------------- 1 | import 'babel/polyfill'; 2 | import React from 'react'; 3 | import FastClick from 'fastclick'; 4 | import Router from 'react-router'; 5 | import routes from './routes/react-routes'; 6 | import injectTapEventPlugin from 'react-tap-event-plugin'; 7 | 8 | let onSetMeta = (name, content) => { 9 | // Remove and create a new tag in order to make it work 10 | // with bookmarks in Safari 11 | let elements = document.getElementsByTagName('meta'); 12 | [].slice.call(elements).forEach((element) => { 13 | if (element.getAttribute('name') === name) { 14 | element.parentNode.removeChild(element); 15 | } 16 | }); 17 | let meta = document.createElement('meta'); 18 | meta.setAttribute('name', name); 19 | meta.setAttribute('content', content); 20 | document.getElementsByTagName('head')[0].appendChild(meta); 21 | }; 22 | 23 | 24 | function run() { 25 | //Needed for onTouchTap 26 | //Can go away when react 1.0 release 27 | //Check this repo: 28 | //https://github.com/zilverline/react-tap-event-plugin 29 | injectTapEventPlugin(); 30 | 31 | // Mesos requests that trigger the Flux worflow passing the data through. 32 | let fluxPropagator = require('./routes/api/mesosFluxPropagator'); 33 | fluxPropagator.propagateMesosData(); 34 | 35 | //Needed for React Developer Tools 36 | window.React = React; 37 | Router.run(routes, Router.HashHistory, function(Handler, state) { 38 | 39 | React.render( document.title = value, 42 | onSetMeta 43 | }} 44 | {...state} />, document.getElementById('app')); 45 | }); 46 | } 47 | 48 | // Run the application when both DOM is ready 49 | // and page content is loaded 50 | Promise.all([ 51 | new Promise((resolve) => { 52 | if (window.addEventListener) { 53 | window.addEventListener('DOMContentLoaded', resolve); 54 | } else { 55 | window.attachEvent('onload', resolve); 56 | } 57 | }).then(() => FastClick.attach(document.body)) 58 | ]).then(run); 59 | -------------------------------------------------------------------------------- /src/components/App/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { RouteHandler } from 'react-router'; 3 | import withContext from '../../decorators/withContext'; 4 | import mui, { FontIcon } from 'material-ui'; 5 | import Logo from '../Logo'; 6 | import Navigation from '../Navigation'; 7 | import Radium from 'radium'; 8 | import ClusterStore from '../../stores/ClusterStore'; 9 | import {Motion, spring} from 'react-motion'; 10 | import ZookeeperRedirect from '../ZookeeperRedirect'; 11 | 12 | let ThemeManager = new mui.Styles.ThemeManager(); 13 | 14 | @withContext 15 | @Radium 16 | class App extends React.Component { 17 | 18 | static propTypes = { 19 | navMedium: React.PropTypes.number, 20 | navSmall: React.PropTypes.number, 21 | motionStiffness: React.PropTypes.number, 22 | motionDamping: React.PropTypes.number 23 | }; 24 | 25 | static childContextTypes = { 26 | muiTheme: React.PropTypes.object 27 | }; 28 | 29 | static defaultProps = { 30 | navMedium: 170, 31 | navSmall: 64, 32 | motionStiffness: 390, 33 | motionDamping: 35 34 | }; 35 | 36 | constructor() { 37 | super(); 38 | this.handleResize = this.handleResize.bind(this); 39 | this.handleClick = this.handleClick.bind(this); 40 | this.state = { 41 | stats: ClusterStore.getStats(), 42 | logs: ClusterStore.getLogs(), 43 | frameworks: ClusterStore.getFrameworks(), 44 | tasks: ClusterStore.getTasks(), 45 | nodes: ClusterStore.getNodes(), 46 | leader: ClusterStore.getLeader(), 47 | pid: ClusterStore.getPid(), 48 | }; 49 | this.redirect = 3000; 50 | } 51 | 52 | getChildContext() { 53 | return { 54 | muiTheme: ThemeManager.getCurrentTheme() 55 | }; 56 | } 57 | 58 | componentDidMount() { 59 | this.mounted = true; 60 | 61 | window.addEventListener('resize', this.handleResize); 62 | this.handleResize(); 63 | 64 | //TODO: we are rerendering components too many times 65 | // everytime we make a mesos request with current approach. 66 | ClusterStore.addChangeListener(this.refreshStats.bind(this)); 67 | ClusterStore.addChangeListener(this.refreshLogs.bind(this)); 68 | ClusterStore.addChangeListener(this.refreshState.bind(this)); 69 | } 70 | 71 | refreshState() { 72 | if (this.mounted) { 73 | this.setState( { 74 | frameworks: ClusterStore.getFrameworks(), 75 | tasks: ClusterStore.getTasks(), 76 | nodes: ClusterStore.getNodes(), 77 | leader: ClusterStore.getLeader(), 78 | pid: ClusterStore.getPid(), 79 | }); 80 | } 81 | } 82 | 83 | refreshStats() { 84 | if (this.mounted) { 85 | this.setState( { stats: ClusterStore.getStats() }); 86 | } 87 | } 88 | 89 | refreshLogs() { 90 | if (this.mounted) { 91 | this.setState( { logs: ClusterStore.getLogs() }); 92 | } 93 | } 94 | 95 | handleResize() { 96 | let widthCurrent = window.matchMedia('(min-width: 1024px)').matches; 97 | if (this.state.widthMedium !== widthCurrent) { 98 | this.setState({widthMedium: widthCurrent}); 99 | } 100 | if (this.state.leftNavExpanded !== widthCurrent) { 101 | this.setState({leftNavExpanded: widthCurrent}); 102 | } 103 | } 104 | 105 | handleClick() { 106 | this.setState({leftNavExpanded: !this.state.leftNavExpanded}); 107 | } 108 | 109 | componentWillUnMount() { 110 | window.removeEventListener('resize', this.handleResize); 111 | ClusterStore.removeChangeListener(this.refreshStats.bind(this)); 112 | ClusterStore.removeChangeListener(this.refreshLogs.bind(this)); 113 | ClusterStore.removeChangeListener(this.refreshState.bind(this)); 114 | this.mounted = false; 115 | } 116 | 117 | menuItems() { 118 | let iconStyle = { 119 | top: 8, 120 | marginRight: 10 121 | }; 122 | let dashboardIcon = React.createElement(FontIcon, {style: iconStyle, className: 'material-icons'}, 'settings_input_svideo' ); 123 | let nodesIcon = React.createElement(FontIcon, {style: iconStyle, className: 'material-icons'}, 'dns' ); 124 | let TasksIcon = React.createElement(FontIcon, {style: iconStyle, className: 'material-icons'}, 'track_changes' ); 125 | let frameworksIcon = React.createElement(FontIcon, {style: iconStyle, className: 'material-icons'}, 'schedule' ); 126 | let logsIcon = React.createElement(FontIcon, {style: iconStyle, className: 'material-icons'}, 'assignment ' ); 127 | 128 | let logsText = this.state.leftNavExpanded ? 'Logs' : ''; 129 | let dashboardText = this.state.leftNavExpanded ? 'Dashboard' : ''; 130 | let frameworksText = this.state.leftNavExpanded ? 'Frameworks' : ''; 131 | let nodesText = this.state.leftNavExpanded ? 'Nodes' : ''; 132 | let tasksText = this.state.leftNavExpanded ? 'Tasks' : ''; 133 | 134 | return [ 135 | { route: '/', text: dashboardText, icon: dashboardIcon }, 136 | { route: 'frameworks', text: frameworksText, icon: frameworksIcon }, 137 | { route: 'nodes', text: nodesText, icon: nodesIcon }, 138 | { route: 'tasks', text: tasksText, icon: TasksIcon }, 139 | { route: 'logs', text: logsText, icon: logsIcon } 140 | ]; 141 | } 142 | 143 | getStyle() { 144 | let burgerColor = this.state.leftNavExpanded ? '#000000' : '#9e9e9e'; 145 | let style = { 146 | logo: { 147 | padding: '10px 12px' 148 | }, 149 | burger: { 150 | display: 'block', 151 | padding: '14px 0 14px 24px', 152 | color: burgerColor, 153 | background: '#ffffff', 154 | cursor: 'pointer', 155 | '@media (minWidth: 1024px)': { 156 | display: 'none' 157 | } 158 | }, 159 | 'menuDivider': { 160 | width: '100%', 161 | borderBottom: '2px solid #9e9e9e', 162 | '@media (minWidth: 1024px)': { 163 | display: 'none' 164 | } 165 | }, 166 | columns: { 167 | position: 'relative', 168 | padding: '20px 0 0 10px', 169 | '@media (minWidth: 768px)': { 170 | padding: '20px 0 0 24px' 171 | }, 172 | '@media (minWidth: 1024px)': { 173 | padding: '20px 24px 0' 174 | } 175 | }, 176 | githubIcon: { 177 | position: 'absolute', 178 | right: 0, 179 | top: '17px', 180 | width: '22px', 181 | fontSize: '180%', 182 | overflow: 'hidden', 183 | '@media (minWidth: 1024px)': { 184 | right: '23px' 185 | } 186 | } 187 | }; 188 | return style; 189 | } 190 | 191 | render() { 192 | let style = this.getStyle(); 193 | return ( 194 |
195 |
196 | 199 | {({thisWidth}) => 200 |
208 | 209 |
213 | menu 214 |
215 |
216 | 217 |
218 | } 219 |
220 | 221 | 224 | {({thisMargin}) => 225 |
226 | 227 |
228 | 229 | 233 | GitHub 234 | 235 | 243 |
244 |
245 | } 246 |
247 | 248 |
249 |
250 | ); 251 | } 252 | 253 | } 254 | 255 | export default App; 256 | -------------------------------------------------------------------------------- /src/components/App/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "App", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./App.js" 6 | } 7 | -------------------------------------------------------------------------------- /src/components/Circle/Circle.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | class Circle extends React.Component { 4 | 5 | static propTypes = { 6 | radius: PropTypes.number.isRequired, 7 | color: PropTypes.string.isRequired 8 | }; 9 | 10 | constructor(props) { 11 | super(props); 12 | } 13 | 14 | render() { 15 | const style = { 16 | background: 'radial-gradient(' + this.props.color +' 5%, hsla(0, 100%, 20%, 0) 90%) 0 0', 17 | width: this.props.radius, 18 | height: this.props.radius, 19 | borderRadius: this.props.radius, 20 | margin: 3, 21 | borderWidth: 1, 22 | borderStyle: 'solid', 23 | borderColor: this.props.color, 24 | float: 'left' 25 | }; 26 | return (
); 27 | } 28 | } 29 | 30 | export default Circle; 31 | -------------------------------------------------------------------------------- /src/components/Circle/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Circle", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./Circle.js" 6 | } 7 | -------------------------------------------------------------------------------- /src/components/DashboardBox/DashboardBox.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { Card, CardTitle } from 'material-ui'; 3 | import _ from 'lodash'; 4 | 5 | class DashboardBox extends React.Component { 6 | static propTypes = { 7 | title: PropTypes.string, 8 | styles: PropTypes.object, 9 | children: PropTypes.object, 10 | xsColSize: PropTypes.number, 11 | smColSize: PropTypes.number, 12 | mdColSize: PropTypes.number, 13 | lgColSize: PropTypes.number 14 | }; 15 | 16 | static defaultProps = { 17 | xsColSize: 12, 18 | smColSize: 6, 19 | mdColSize: 4, 20 | styles: {} 21 | }; 22 | 23 | constructor(props) { 24 | super(props); 25 | } 26 | 27 | getStyles() { 28 | return { 29 | root: { 30 | borderRadius: 5, 31 | minHeight: 315 32 | }, 33 | card: { 34 | titleColor: '#000000', 35 | subtitleColor: '#000000' 36 | } 37 | }; 38 | } 39 | 40 | getCols() { 41 | let xs = this.props.xsColSize; 42 | let sm = this.props.smColSize; 43 | let md = this.props.mdColSize; 44 | let lg = this.props.lgColSize; 45 | 46 | let cols = 'col-xs-' + xs; 47 | if (sm) { 48 | cols = cols + ' col-sm-' + sm; 49 | } 50 | if (md) { 51 | cols = cols + ' col-md-' + md; 52 | } 53 | if (lg) { 54 | cols = cols + ' col-lg-' + lg; 55 | } 56 | return cols; 57 | } 58 | 59 | render() { 60 | let styles = _.merge(this.getStyles(), this.props.styles); 61 | 62 | return ( 63 |
64 |
65 | 66 | 67 | {this.props.children} 68 | 69 |
70 |
71 | ); 72 | } 73 | 74 | } 75 | 76 | export default DashboardBox; 77 | -------------------------------------------------------------------------------- /src/components/DashboardBox/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DashboardBox", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./DashboardBox.js" 6 | } 7 | -------------------------------------------------------------------------------- /src/components/Donut/Donut.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import d3 from 'd3'; 3 | import { Styles } from 'material-ui'; 4 | let { Colors } = Styles; 5 | import Legend from './Legend'; 6 | import _ from 'lodash'; 7 | 8 | class Donut extends React.Component { 9 | static propTypes = { 10 | width: React.PropTypes.number, 11 | height: React.PropTypes.number, 12 | title: React.PropTypes.string, 13 | data: React.PropTypes.array.isRequired, 14 | colors: React.PropTypes.object, 15 | transitionDuration: React.PropTypes.number 16 | }; 17 | 18 | static contextTypes = { 19 | muiTheme: React.PropTypes.object 20 | }; 21 | 22 | static defaultProps = { 23 | width: 200, 24 | height: 200, 25 | title: '', 26 | data: [], 27 | transitionDuration: 1000, 28 | colors: { 29 | green: Colors.green500, 30 | red: Colors.red500, 31 | amber: Colors.amber500, 32 | grey: Colors.grey300, 33 | deepPurple: Colors.deepPurple700, 34 | cyan: Colors.cyan500, 35 | orange: Colors.orange500 36 | } 37 | }; 38 | 39 | constructor(props) { 40 | super(props); 41 | this.update = this.update.bind(this); 42 | this.drawInitialComponent = this.drawInitialComponent.bind(this); 43 | } 44 | 45 | componentDidMount() { 46 | let props = this.props; 47 | let data = props.data; 48 | 49 | // If we have data draw the donut. 50 | if(data.filter(function(d) { return d.count > 0; }).length > 0) { 51 | this.drawInitialComponent(props); 52 | } 53 | } 54 | 55 | shouldComponentUpdate(nextProps) { 56 | let oldValues = this.props.data.map(function(item){ 57 | return item.count; 58 | }); 59 | let newValues = nextProps.data.map(function(item){ 60 | return item.count; 61 | }); 62 | 63 | if (!(_.isEqual(oldValues, newValues))) { 64 | this.update(nextProps); 65 | return false; 66 | } 67 | return true; 68 | } 69 | 70 | getDonutLegend() { 71 | let data = this.props.data; 72 | let colors = this.props.colors; 73 | return data.map(function(item, i){ 74 | return React.createElement(Legend, { key: i, color: colors[item.color], item: item }); 75 | }); 76 | } 77 | 78 | drawInitialComponent(props) { 79 | let colors = props.colors; 80 | let data = props.data; 81 | let paperColour = this.context.muiTheme.component.paper.backgroundColor; 82 | let transitionDuration = props.transitionDuration; 83 | 84 | let color = data.map(function(item){ 85 | return colors[item.color]; 86 | }); 87 | 88 | let svg = d3.select(React.findDOMNode(this.refs.svg)) 89 | .append('g') 90 | .attr('transform', 'translate(' + (props.width) / 2 + ',' + (props.height) / 2 + ')'); 91 | 92 | let d3Colors = d3.scale.ordinal().range(color); 93 | let outerRadius = props.width / 2.2; 94 | let innerRadius = props.width / 2; 95 | let arc = d3.svg.arc() 96 | .outerRadius(outerRadius) 97 | .innerRadius(innerRadius); 98 | 99 | let pie = d3.layout.pie() 100 | .value(function(d) { return d.count; }) 101 | .sort(null); 102 | 103 | this.path = svg.datum(data).selectAll('path') 104 | .data(pie(data)) 105 | .enter() 106 | .append('path') 107 | .attr('fill', function(d, i) { return d3Colors(i); }) 108 | .attr('d', arc) 109 | .each(function(d) { 110 | this.current = d; 111 | }); 112 | 113 | this.path.transition() 114 | .ease('exp') 115 | .duration(transitionDuration) 116 | .attrTween('d', function(b) { 117 | var i = d3.interpolate({startAngle: 1.1 * Math.PI, endAngle: 1.1 * Math.PI}, b); 118 | return function(t) { 119 | return arc(i(t)); 120 | }; 121 | }); 122 | 123 | // only draw a stroke around the arcs if we have 2 data points that have a 124 | // count greater than zero. Otherwise we draw a stroke round a full arc 125 | // which displays a bit odd. 126 | if(data.filter(function(d) { return d.count > 0; }).length > 1) { 127 | this.path.style('stroke', paperColour); 128 | } 129 | } 130 | 131 | update(props) { 132 | let data = props.data; 133 | let width = props.width; 134 | let transitionDuration = props.transitionDuration; 135 | let paperColour = this.context.muiTheme.component.paper.backgroundColor; 136 | let outerRadius = width / 2.1; 137 | let innerRadius = width / 2; 138 | let arc = d3.svg.arc().outerRadius(outerRadius).innerRadius(innerRadius); 139 | 140 | let pie = d3.layout.pie() 141 | .value(function(d) { return d.count; }) 142 | .sort(null); 143 | 144 | // Check if we've already drawn the component. 145 | if (typeof this.path === 'undefined') { 146 | this.drawInitialComponent(props); 147 | } else { 148 | // Compute the new angles and do the transition. 149 | this.path.data(pie(data)) 150 | .transition() 151 | .duration(transitionDuration) 152 | .ease('exp') 153 | .attrTween('d', function arcTween(a) { 154 | var i = d3.interpolate(this.current, a); 155 | this.current = i(0); 156 | return function(t) { 157 | return arc(i(t)); 158 | }; 159 | }); 160 | 161 | // only draw a stroke around the arcs if we have 2 data points that have a 162 | // count greater than zero. Otherwise we draw a stroke round a full arc 163 | // which displays a bit odd. 164 | if(data.filter(function(d) { return d.count > 0; }).length > 1) { 165 | this.path.style('stroke', paperColour); 166 | } 167 | else { 168 | this.path.style('stroke', 'none'); 169 | } 170 | } 171 | } 172 | 173 | getStyles() { 174 | return { 175 | largeText: { 176 | fontSize: 30 177 | }, 178 | smallText: { 179 | fontSize: 15 180 | } 181 | }; 182 | } 183 | 184 | render() { 185 | let props = this.props; 186 | let data = props.data; 187 | 188 | let styles = this.getStyles(); 189 | let palette = this.context.muiTheme.palette; 190 | let title = props.title; 191 | let result = data.map(function(item){ 192 | return item.count; 193 | }); 194 | let sum = result.reduce(function(memo, num){ 195 | return _.round(memo + num, 2); 196 | }, 0); 197 | let position = 'translate(' + (props.width) / 2 + ',' + (props.height) / 2 + ')'; 198 | 199 | return ( 200 |
201 |
202 | 203 | 204 | {sum} 205 | {title} 206 | 207 | 208 |
209 |
210 | {this.getDonutLegend()} 211 |
212 |
213 | ); 214 | } 215 | } 216 | 217 | export default Donut; 218 | -------------------------------------------------------------------------------- /src/components/Donut/Legend.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Legend extends React.Component { 4 | 5 | static propTypes = { 6 | item: React.PropTypes.object, 7 | color: React.PropTypes.string 8 | }; 9 | 10 | getStyles() { 11 | let styles = { 12 | itemName: { 13 | color: this.props.color 14 | }, 15 | itemCount: { 16 | marginRight: 5 17 | } 18 | }; 19 | return styles; 20 | } 21 | 22 | render() { 23 | let item = this.props.item; 24 | let styles = this.getStyles(); 25 | 26 | return ( 27 |
28 | {item.count} 29 | {item.name} 30 |
31 | ); 32 | } 33 | } 34 | 35 | export default Legend; 36 | -------------------------------------------------------------------------------- /src/components/Donut/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Donut", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./Donut.js" 6 | } 7 | -------------------------------------------------------------------------------- /src/components/FrameworkBlock/FrameworkBlock.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { /*Styles, */RaisedButton } from 'material-ui'; 3 | 4 | class FrameworkBlock extends React.Component { 5 | 6 | static propTypes = { 7 | name: PropTypes.string.isRequired, 8 | logoFile: PropTypes.string, 9 | version: PropTypes.string, 10 | tasks: PropTypes.number, 11 | url: React.PropTypes.string 12 | }; 13 | 14 | static contextTypes = { 15 | muiTheme: PropTypes.object 16 | } 17 | 18 | defaultLogo() { 19 | return 'master/static/assets/icon-framework-' + this.props.name + '.png'; 20 | } 21 | 22 | render() { 23 | let props = this.props; 24 | let name = props.name; 25 | let version = props.version; 26 | let tasks = props.tasks; 27 | let webUrl = props.url; 28 | let logo = props.logoFile || this.defaultLogo(); 29 | 30 | let appVersion = ''; 31 | let appTasks = null; 32 | let appUrl = null; 33 | 34 | if (typeof version !== 'undefined') { 35 | appVersion = 'v' + version; 36 | } 37 | 38 | if (typeof tasks !== 'undefined') { 39 | appTasks = React.createElement('span', {}, '(' + tasks + ' Tasks)'); 40 | } 41 | 42 | if (typeof webUrl !== 'undefined') { 43 | // Create a button/link element. 44 | appUrl = React.createElement(RaisedButton, { linkButton: true, 45 | href: webUrl, 46 | label: 'Open', 47 | secondary: true } 48 | ); 49 | } 50 | 51 | let appLogo = React.createElement('div', { className: 'center-xs logo'}, 52 | React.createElement('img', { width: 70, height: 70, src: logo})); 53 | 54 | return ( 55 |
56 | {appLogo} 57 |
58 |
    59 | {appTasks} 60 |
  • {name} {appVersion}
  • 61 | {appUrl} 62 |
63 |
64 |
65 | ); 66 | } 67 | 68 | } 69 | 70 | export default FrameworkBlock; 71 | -------------------------------------------------------------------------------- /src/components/FrameworkBlock/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "FrameworkBlock", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./FrameworkBlock.js" 6 | } 7 | -------------------------------------------------------------------------------- /src/components/Logo/Logo.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | class Logo extends React.Component { 4 | static propTypes = { 5 | height: PropTypes.number, 6 | width: PropTypes.number, 7 | styles: PropTypes.object 8 | }; 9 | 10 | static defaultProps = { 11 | height: 256, 12 | width: 291, 13 | styles: { 14 | backColour: '#FFFFFF', 15 | fillColour: '#00AEDE' 16 | } 17 | }; 18 | 19 | logoStyles() { 20 | let styles = { 21 | 'root': { 22 | backgroundColor: '#004561' 23 | }, 24 | 'svg': { 25 | padding: '10px 0 4px 13px' 26 | } 27 | }; 28 | return styles; 29 | }; 30 | 31 | render() { 32 | let props = this.props; 33 | let styles = props.styles; 34 | let logoStyles = this.logoStyles(); 35 | let viewBox = '0 0 256 291'; 36 | 37 | return ( 38 |
39 | 40 | Mesos UI 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 |
69 | ); 70 | } 71 | } 72 | 73 | export default Logo; 74 | -------------------------------------------------------------------------------- /src/components/Logo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Logo", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./Logo.js" 6 | } 7 | -------------------------------------------------------------------------------- /src/components/Navigation/Navigation.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Menu } from 'material-ui'; 3 | 4 | class Navigation extends React.Component { 5 | 6 | static propTypes = { 7 | menuItems: React.PropTypes.array 8 | }; 9 | 10 | static contextTypes = { 11 | router: React.PropTypes.func, 12 | muiTheme: React.PropTypes.object 13 | }; 14 | 15 | constructor() { 16 | super(); 17 | this.getSelectedIndex = this.getSelectedIndex.bind(this); 18 | this.onMenuItemClick = this.onMenuItemClick.bind(this); 19 | } 20 | 21 | getSelectedIndex() { 22 | let menuItems = this.props.menuItems; 23 | let currentItem; 24 | 25 | for (let i = menuItems.length - 1; i >= 0; i--) { 26 | currentItem = menuItems[i]; 27 | if (currentItem.route && this.context.router.isActive(currentItem.route)) { 28 | return i; 29 | } 30 | } 31 | } 32 | 33 | onMenuItemClick(e, index, item) { 34 | this.context.router.transitionTo(item.route); 35 | } 36 | 37 | getStyles() { 38 | return { 39 | minHeight: '800', 40 | borderRadius: 0, 41 | zIndex: 0, 42 | boxShadow: 'none', 43 | whiteSpace: 'pre' 44 | }; 45 | } 46 | 47 | render() { 48 | return ( 49 | 58 | ); 59 | } 60 | 61 | } 62 | 63 | export default Navigation; 64 | -------------------------------------------------------------------------------- /src/components/Navigation/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Navigation", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./Navigation.js" 6 | } 7 | -------------------------------------------------------------------------------- /src/components/PageTitle/PageTitle.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | class PageTitle extends React.Component { 4 | 5 | static propTypes = { 6 | title: PropTypes.string.isRequired 7 | }; 8 | 9 | /* @todo - allow overridable styles */ 10 | getStyles() { 11 | let styles = { 12 | fontWeight: 100, 13 | marginBottom: 20 14 | }; 15 | return styles; 16 | } 17 | 18 | render() { 19 | let title = this.props.title; 20 | let styles = this.getStyles(); 21 | 22 | return ( 23 |

{title}

24 | ); 25 | } 26 | } 27 | 28 | export default PageTitle; 29 | -------------------------------------------------------------------------------- /src/components/PageTitle/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PageTitle", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./PageTitle.js" 6 | } 7 | -------------------------------------------------------------------------------- /src/components/Sunburst/Legend.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import { Styles, Utils } from 'material-ui'; 3 | let { Colors } = Styles; 4 | import _ from 'lodash'; 5 | import LegendItem from './LegendItem.js'; 6 | 7 | class Legend extends React.Component { 8 | static propTypes = { 9 | totalResources: PropTypes.object.isRequired, 10 | usedResources: PropTypes.object.isRequired, 11 | colors: PropTypes.object 12 | }; 13 | 14 | static contextTypes = { 15 | muiTheme: PropTypes.object 16 | }; 17 | 18 | static defaultProps = { 19 | colors: { 20 | cpus: Colors.deepPurple700, /* @todo pull this from the theme */ 21 | mem: Colors.cyan500, 22 | disk: Colors.orange500 23 | } 24 | }; 25 | 26 | getTitleMapping(key) { 27 | let titles = { 28 | cpus: 'CPU', 29 | disk: 'Disk', 30 | mem: 'Memory' 31 | }; 32 | 33 | return titles[key]; 34 | } 35 | 36 | render() { 37 | let props = this.props; 38 | let totalResources = _.omit(props.totalResources, 'ports', 'ephemeral_ports'); 39 | let usedResources = _.omit(props.usedResources, 'ports', 'ephemeral_ports'); 40 | let legendItems = []; 41 | let _this = this; //eslint-disable-line 42 | 43 | _.forIn(totalResources, function(value, key) { 44 | legendItems.push(React.createElement(LegendItem, {key: key, title: _this.getTitleMapping(key), 45 | color: props.colors[key], total: value, used: usedResources[key] })); 46 | }); 47 | 48 | return ( 49 |
50 | {legendItems} 51 |
52 | ); 53 | } 54 | } 55 | 56 | export default Legend; 57 | -------------------------------------------------------------------------------- /src/components/Sunburst/LegendItem.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | class LegendItem extends React.Component { 4 | static propTypes = { 5 | title: PropTypes.string.isRequired, 6 | used: PropTypes.number, 7 | total: PropTypes.number, 8 | color: PropTypes.string 9 | }; 10 | 11 | static contextTypes = { 12 | muiTheme: PropTypes.object 13 | }; 14 | 15 | getStyles() { 16 | let color = this.props.color; 17 | return { 18 | root: { 19 | listStyle: 'none', 20 | marginBottom: 5 21 | }, 22 | title: { 23 | float: 'left', 24 | width: 60 25 | }, 26 | img: { 27 | float: 'left', 28 | marginRight: 5, 29 | backgroundColor: color, 30 | width: 20, 31 | height: 20 32 | } 33 | }; 34 | } 35 | 36 | render() { 37 | let props = this.props; 38 | let title = props.title; 39 | let used = props.used; 40 | let total = props.total; 41 | let styles = this.getStyles(); 42 | 43 | return ( 44 |
  • 45 | {title} {used} / {total} 46 |
  • 47 | ); 48 | } 49 | } 50 | 51 | export default LegendItem; 52 | -------------------------------------------------------------------------------- /src/components/Sunburst/Sunburst.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import d3 from 'd3'; 3 | import _ from 'lodash'; 4 | import Legend from './Legend.js'; 5 | import { Styles, Utils } from 'material-ui'; 6 | let { Colors } = Styles; 7 | let { ColorManipulator } = Utils; 8 | 9 | class Sunburst extends React.Component { 10 | static propTypes = { 11 | width: React.PropTypes.number, 12 | height: React.PropTypes.number, 13 | totalResources: React.PropTypes.object.isRequired, 14 | usedResources: React.PropTypes.object.isRequired, 15 | colors: React.PropTypes.object, 16 | transitionDuration: React.PropTypes.number 17 | }; 18 | 19 | static contextTypes = { 20 | muiTheme: React.PropTypes.object 21 | }; 22 | 23 | static defaultProps = { 24 | width: 200, 25 | height: 200, 26 | data: [], 27 | transitionDuration: 1000, 28 | colors: { 29 | cpus: Colors.deepPurple700, /* @todo pull this from the theme */ 30 | mem: Colors.cyan500, 31 | disk: Colors.orange500, 32 | canvas: ColorManipulator.fade(Colors.darkBlack, 0.1) 33 | } 34 | }; 35 | 36 | constructor(props) { 37 | super(props); 38 | this.drawInitialComponent = this.drawInitialComponent.bind(this); 39 | this.arcTween = this.arcTween.bind(this); 40 | this.updateDonuts = this.updateDonuts.bind(this); 41 | this.paths = []; 42 | this.arcs = []; 43 | } 44 | 45 | componentDidMount() { 46 | this.drawInitialComponent(this.props); 47 | } 48 | 49 | shouldComponentUpdate(nextProps) { 50 | this.updateDonuts(nextProps); 51 | // skips react render() function. 52 | return false; 53 | } 54 | 55 | arcTween(transition, newAngle, arc) { 56 | transition.attrTween('d', function(d) { 57 | var interpolate = d3.interpolate(d.endAngle, newAngle); 58 | return function(t) { 59 | d.endAngle = interpolate(t); 60 | return arc(d); 61 | }; 62 | }); 63 | } 64 | 65 | drawInitialComponent(props) { 66 | let twoPie = 2 * Math.PI; 67 | let totalResources = _.omit(props.totalResources, 'ports', 'ephemeral_ports'); 68 | let usedResources = _.omit(props.usedResources, 'ports', 'ephemeral_ports'); 69 | let outerRadius = props.width / 2.; 70 | let innerRadius = props.width / 2.24; 71 | let radiusDifference = 12; 72 | let index = 0; 73 | let _this = this; //eslint-disable-line 74 | let colors = this.props.colors; 75 | let canvasColour = colors.canvas; 76 | 77 | let svg = d3.select(React.findDOMNode(this.refs.svg)) 78 | .append('g') 79 | .attr('transform', 'translate(' + (props.width) / 2 + ',' + (props.height) / 2 + ')'); 80 | 81 | // Loop through each resource and compute a pie chart for it. 82 | _.forIn(totalResources, function(value, key) { 83 | 84 | _this.arcs[key] = d3.svg.arc() 85 | .outerRadius(outerRadius - (index * radiusDifference)) 86 | .innerRadius(innerRadius - (index * radiusDifference)) 87 | .startAngle(0); 88 | 89 | // background path fill. 90 | svg.append('path') 91 | .datum({endAngle: twoPie}) 92 | .style('fill', canvasColour) 93 | .attr('class', key) 94 | .attr('d', _this.arcs[key]); 95 | 96 | let linearScale = d3.scale.linear().domain([0, value]); 97 | let newAngle = linearScale(usedResources[key]) * twoPie; 98 | 99 | // foreground path fill. 100 | _this.paths[key] = svg.append('path') 101 | .datum({endAngle: newAngle}) 102 | .style('fill', colors[key]) 103 | .attr('d', _this.arcs[key]); 104 | 105 | index++; 106 | }); 107 | } 108 | 109 | updateDonuts(props) { 110 | let twoPie = 2 * Math.PI; 111 | let totalResources = _.omit(props.totalResources, 'ports', 'ephemeral_ports'); 112 | let usedResources = _.omit(props.usedResources, 'ports', 'ephemeral_ports'); 113 | let transitionDuration = props.transitionDuration; 114 | let _this = this; //eslint-disable-line 115 | 116 | // Loop through each resource update the donut for each. 117 | _.forIn(totalResources, function(value, key) { 118 | 119 | // This calculates the percent value (e.g. 0.25 for 25%) for the arc 120 | let linearScale = d3.scale.linear().domain([0, value]); 121 | let newAngle = linearScale(usedResources[key]) * twoPie; 122 | 123 | // transition to the new angle. 124 | _this.paths[key].transition() 125 | .duration(transitionDuration) 126 | .call(_this.arcTween, newAngle, _this.arcs[key]); 127 | }); 128 | } 129 | 130 | render() { 131 | let props = this.props; 132 | 133 | return ( 134 |
    135 |
    136 | 137 | 138 |
    139 |
    140 | 141 |
    142 |
    143 | ); 144 | } 145 | } 146 | 147 | export default Sunburst; 148 | -------------------------------------------------------------------------------- /src/components/Sunburst/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Sunburst", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./Sunburst.js" 6 | } 7 | -------------------------------------------------------------------------------- /src/components/TaskTable/TaskTable.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import Table from 'material-ui/lib/table/table'; 3 | import TableHeaderColumn from 'material-ui/lib/table/table-header-column'; 4 | import TableRow from 'material-ui/lib/table/table-row'; 5 | import TableHeader from 'material-ui/lib/table/table-header'; 6 | import TableRowColumn from 'material-ui/lib/table/table-row-column'; 7 | import TableBody from 'material-ui/lib/table/table-body'; 8 | import RadioButton from 'material-ui/lib/radio-button'; 9 | import RadioButtonGroup from 'material-ui/lib/radio-button-group'; 10 | 11 | class TaskTable extends React.Component { 12 | 13 | static propTypes = { 14 | tasks: PropTypes.array.isRequired 15 | }; 16 | 17 | constructor() { 18 | super(); 19 | this.state = { 20 | parameterToOrderBy: 'timestamp', 21 | }; 22 | } 23 | 24 | setParameterToOrderBy(e) { 25 | this.setState({parameterToOrderBy: e.target.value}); 26 | } 27 | 28 | orderTasksByParameter(tasks = [], parameterToOrderBy = 'timestamp') { 29 | var parameter = parameterToOrderBy; 30 | tasks.sort(function(a, b) { 31 | if (a[parameter] < b[parameter]) { 32 | return 1; 33 | } 34 | if (a[parameter] > b[parameter]) { 35 | return -1; 36 | } 37 | return 0; 38 | }); 39 | return tasks; 40 | } 41 | 42 | renderOrderBox() { 43 | // We Can't just order on table header clicks due to: 44 | //https://github.com/callemall/material-ui/issues/1783 45 | //https://github.com/callemall/material-ui/issues/2011 46 | return( 47 |
    48 | Order tasks by: 49 | 50 | 56 | 62 | 68 | 69 |
    70 | ); 71 | } 72 | 73 | getStyles() { 74 | return { 75 | radioButton: { 76 | width: 150, 77 | float: 'left' 78 | 79 | }, 80 | orderBox: { 81 | height: 30, 82 | padding: 20 83 | } 84 | }; 85 | } 86 | 87 | //http://stackoverflow.com/questions/3177836/how-to-format-time-since-xxx-e-g-4-minutes-ago-similar-to-stack-exchange-site 88 | timeSince(date) { 89 | 90 | var seconds = Math.floor((new Date() - date*1000) / 1000); 91 | 92 | var interval = Math.floor(seconds / 31536000); 93 | 94 | if (interval > 1) { 95 | return interval + ' years'; 96 | } 97 | interval = Math.floor(seconds / 2592000); 98 | if (interval > 1) { 99 | return interval + ' months'; 100 | } 101 | interval = Math.floor(seconds / 86400); 102 | if (interval > 1) { 103 | return interval + ' days'; 104 | } 105 | interval = Math.floor(seconds / 3600); 106 | if (interval > 1) { 107 | return interval + ' hours'; 108 | } 109 | interval = Math.floor(seconds / 60); 110 | if (interval > 1) { 111 | return interval + ' minutes'; 112 | } 113 | return Math.floor(seconds) + ' seconds'; 114 | } 115 | 116 | 117 | render() { 118 | 119 | let tasks = this.props.tasks.slice(); 120 | let orderedTasks = this.orderTasksByParameter(tasks, this.state.parameterToOrderBy); 121 | return ( 122 |
    123 | {this.renderOrderBox()} 124 | 125 | 126 | 127 | ID 128 | Name 129 | Status 130 | Up time 131 | Framework 132 | Host 133 | 134 | 135 | 136 | {orderedTasks.map((task) => 137 | 138 | {task.id} 139 | {task.name} 140 | {task.state} 141 | {this.timeSince(task.timestamp)} 142 | {task.framework_name} 143 | {task.hostname} 144 | 145 | ) 146 | } 147 | 148 |
    149 |
    150 | ); 151 | } 152 | } 153 | 154 | export default TaskTable; 155 | 156 | -------------------------------------------------------------------------------- /src/components/TaskTable/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TaskTable", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./TaskTable.js" 6 | } 7 | -------------------------------------------------------------------------------- /src/components/TaskVisualizer/TaskVisualizer.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext: true */ 2 | 3 | import React, { PropTypes } from 'react'; 4 | import Circle from '../Circle'; 5 | import { List, ListItem } from 'material-ui/lib/lists'; 6 | import Checkbox from 'material-ui/lib/checkbox'; 7 | import Toggle from 'material-ui/lib/toggle'; 8 | import RadioButton from 'material-ui/lib/radio-button'; 9 | import RadioButtonGroup from 'material-ui/lib/radio-button-group'; 10 | 11 | class TaskVisualizer extends React.Component { 12 | 13 | static propTypes = { 14 | tasks: PropTypes.array.isRequired, 15 | }; 16 | 17 | constructor() { 18 | super(); 19 | this.state = { 20 | parameterToGroupBy: 'name', 21 | toggleAppList: {} 22 | }; 23 | } 24 | 25 | groupBy(e) { 26 | this.setState({parameterToGroupBy: e.target.value}); 27 | } 28 | 29 | //http://stackoverflow.com/questions/3426404/create-a-hexadecimal-colour-based-on-a-string-with-javascript 30 | hashCode(str) { // java String#hashCode 31 | var hash = 0; 32 | for (var i = 0; i < str.length; i++) { 33 | hash = str.charCodeAt(i) + ((hash << 5) - hash); 34 | } 35 | return hash; 36 | } 37 | 38 | intToRGB(i){ 39 | var c = (i & 0x00FFFFFF) 40 | .toString(16) 41 | .toUpperCase(); 42 | 43 | return '#' + '00000'.substring(0, 6 - c.length) + c; 44 | } 45 | 46 | stringToColor(string) { 47 | return this.intToRGB(this.hashCode(string)); 48 | } 49 | 50 | // Return a hash appName: HEX_color_value 51 | createAppColorList(tasks) { 52 | let appColorList = {}; 53 | for(var index in tasks) { 54 | let appName = tasks[index].name; 55 | appColorList[appName] = this.stringToColor(appName); 56 | } 57 | return appColorList; 58 | } 59 | 60 | // Keeps track of toggleAppList. 61 | toggleApp(name, e) { 62 | let newStateToggleAppList = this.state.toggleAppList 63 | newStateToggleAppList[name] = !this.state.toggleAppList[name]; 64 | this.setState({toggleAppList: newStateToggleAppList}); 65 | } 66 | 67 | renderAppColorLegend(appColorList) { 68 | 69 | let appColorListRendered = []; 70 | 71 | for (var name in appColorList) { 72 | appColorListRendered.push( 73 |
    74 | 75 | 76 | {name} 77 |
    78 | ); 79 | } 80 | 81 | return ( 82 |
    83 | 84 | 87 | {appColorListRendered} 88 | 89 |
    90 | ); 91 | } 92 | 93 | excludeToggledTasks() { 94 | let tasks = []; 95 | for (var index in this.props.tasks) { 96 | let name = this.props.tasks[index].name; 97 | if (!this.state.toggleAppList[name]) { 98 | tasks.push(this.props.tasks[index]); 99 | } 100 | } 101 | return tasks; 102 | } 103 | 104 | createCirclesInGroupsList() { 105 | 106 | let tasks = this.excludeToggledTasks(); 107 | let appColorList = this.createAppColorList(tasks); 108 | let circlesList = {}; 109 | let key = 0; 110 | 111 | for (var index in tasks ) { 112 | let name = tasks[index].name; 113 | let id = tasks[index].id 114 | let color = appColorList[name]; 115 | let parameterToGroupByValue = tasks[index][this.state.parameterToGroupBy] 116 | 117 | circlesList[parameterToGroupByValue] = circlesList[parameterToGroupByValue] || []; 118 | circlesList[parameterToGroupByValue].push(); 119 | } 120 | 121 | return circlesList 122 | } 123 | 124 | renderCirclesInGroups(circlesList) { 125 | let groupCirclesList = [] 126 | for (var parameterToGroupByValue in circlesList) { 127 | groupCirclesList.push(
    {parameterToGroupByValue}
    {circlesList[parameterToGroupByValue]}
    ); 128 | } 129 | return groupCirclesList; 130 | } 131 | 132 | getStyles(color) { 133 | return { 134 | taskLegendBox: { 135 | margin: 20, 136 | float: 'left' 137 | }, 138 | circleBox: { 139 | width: 270, 140 | margin: 10, 141 | float: 'left' 142 | }, 143 | radioButton: { 144 | marginBottom: 16, 145 | } 146 | }; 147 | } 148 | 149 | render() { 150 | 151 | let tasks = this.props.tasks 152 | return ( 153 |
    154 | {this.renderAppColorLegend(this.createAppColorList(tasks))} 155 |
    156 | 157 | 163 | 169 | 170 |
    171 |
      172 | {this.renderCirclesInGroups(this.createCirclesInGroupsList())} 173 |
    174 |
    175 | ); 176 | } 177 | } 178 | 179 | export default TaskVisualizer; 180 | -------------------------------------------------------------------------------- /src/components/TaskVisualizer/__tests__/TaskVisualizer-test.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext: true */ 2 | jest.dontMock('../TaskVisualizer'); 3 | jest.autoMockOff(); 4 | 5 | describe('TaskVisualizer', function() { 6 | 7 | var React = require('react/addons'); 8 | var TestUtils = React.addons.TestUtils; 9 | var TaskVisualizer = require('../TaskVisualizer.js'); 10 | var tasks = require('./tasks-stub.json'); 11 | 12 | 13 | it('gets Hex colors from app names', function() { 14 | const TaskVisualizer = require('../TaskVisualizer'); 15 | let object = new TaskVisualizer(); 16 | expect(object.stringToColor('anyAppName')).toMatch("#[a-zA-Z0-9]{6,}$"); 17 | }); 18 | 19 | it('creates a hash like => appName: hexColor from an array of tasks.', function() { 20 | const TaskVisualizer = require('../TaskVisualizer'); 21 | let object = new TaskVisualizer(); 22 | let appColorList = object.createAppColorList(tasks); 23 | expect(appColorList.stubby).toMatch("#[a-zA-Z0-9]{6,}$"); 24 | expect(appColorList.dashing).toMatch("#[a-zA-Z0-9]{6,}$"); 25 | expect(appColorList.stubby).toMatch("#[a-zA-Z0-9]{6,}$"); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/components/TaskVisualizer/__tests__/tasks-stub.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "qa-mailhog.bcdf1bbd-e50a-11e5-b92c-c6c68d973162", 4 | "name": "qa-mailhog", 5 | "framework_id": "82cde340-9d3b-40a6-87e2-953952745763-0000", 6 | "executor_id": "", 7 | "slave_id": "02223256-125c-4c3d-81db-3cdafbf233cd-S4", 8 | "state": "TASK_RUNNING", 9 | "resources": { 10 | "cpus": 0.3, 11 | "disk": 0, 12 | "mem": 128, 13 | "ports": "[31385-31386]" 14 | }, 15 | "statuses": [ 16 | { 17 | "state": "TASK_RUNNING", 18 | "timestamp": 1457426990.49369, 19 | "labels": [ 20 | { 21 | "key": "Docker.NetworkSettings.IPAddress", 22 | "value": "10.34.0.8" 23 | } 24 | ], 25 | "container_status": { 26 | "network_infos": [ 27 | { 28 | "ip_address": "10.34.0.8", 29 | "ip_addresses": [ 30 | { 31 | "ip_address": "10.34.0.8" 32 | } 33 | ] 34 | } 35 | ] 36 | } 37 | } 38 | ], 39 | "labels": [ 40 | { 41 | "key": "traefik.frontend.value", 42 | "value": "mailhog.qa.service.vcloud" 43 | } 44 | ], 45 | "container": { 46 | "type": "DOCKER", 47 | "docker": { 48 | "image": "mailhog/mailhog", 49 | "network": "BRIDGE", 50 | "port_mappings": [ 51 | { 52 | "host_port": 31385, 53 | "container_port": 8025, 54 | "protocol": "tcp" 55 | }, 56 | { 57 | "host_port": 31386, 58 | "container_port": 1025, 59 | "protocol": "tcp" 60 | } 61 | ], 62 | "privileged": false, 63 | "force_pull_image": false 64 | } 65 | }, 66 | "framework_name": "marathon", 67 | "hostname": "10.66.1.158", 68 | "timestamp": [ 69 | 1457426990.49369 70 | ] 71 | }, 72 | { 73 | "id": "stubby.46eb7e7b-e1fa-11e5-9a50-5ac2a4ac00a4", 74 | "name": "stubby", 75 | "framework_id": "82cde340-9d3b-40a6-87e2-953952745763-0000", 76 | "executor_id": "", 77 | "slave_id": "02223256-125c-4c3d-81db-3cdafbf233cd-S1", 78 | "state": "TASK_RUNNING", 79 | "resources": { 80 | "cpus": 0.3, 81 | "disk": 0, 82 | "mem": 64, 83 | "ports": "[31209-31209]" 84 | }, 85 | "statuses": [ 86 | { 87 | "state": "TASK_RUNNING", 88 | "timestamp": 1457090071.49399, 89 | "labels": [ 90 | { 91 | "key": "Docker.NetworkSettings.IPAddress", 92 | "value": "10.36.0.10" 93 | } 94 | ], 95 | "container_status": { 96 | "network_infos": [ 97 | { 98 | "ip_address": "10.36.0.10", 99 | "ip_addresses": [ 100 | { 101 | "ip_address": "10.36.0.10" 102 | } 103 | ] 104 | } 105 | ] 106 | } 107 | } 108 | ], 109 | "labels": [ 110 | { 111 | "key": "traefik.frontend.value", 112 | "value": "stubby.{environment:[a-z]+}.service.vcloud" 113 | } 114 | ], 115 | "container": { 116 | "type": "DOCKER", 117 | "docker": { 118 | "image": "docker-registry.service.tmp.vcloud:5000/stubby", 119 | "network": "BRIDGE", 120 | "port_mappings": [ 121 | { 122 | "host_port": 31209, 123 | "container_port": 8008, 124 | "protocol": "tcp" 125 | } 126 | ], 127 | "privileged": false, 128 | "force_pull_image": false 129 | } 130 | }, 131 | "framework_name": "marathon", 132 | "hostname": "10.66.1.159", 133 | "timestamp": [ 134 | 1457090071.49399 135 | ] 136 | }, 137 | { 138 | "id": "dashing.1923ceec-e16d-11e5-9a50-5ac2a4ac00a4", 139 | "name": "dashing", 140 | "framework_id": "82cde340-9d3b-40a6-87e2-953952745763-0000", 141 | "executor_id": "", 142 | "slave_id": "02223256-125c-4c3d-81db-3cdafbf233cd-S1", 143 | "state": "TASK_RUNNING", 144 | "resources": { 145 | "cpus": 0.3, 146 | "disk": 0, 147 | "mem": 128, 148 | "ports": "[31244-31244]" 149 | }, 150 | "statuses": [ 151 | { 152 | "state": "TASK_RUNNING", 153 | "timestamp": 1457029435.12229, 154 | "labels": [ 155 | { 156 | "key": "Docker.NetworkSettings.IPAddress", 157 | "value": "10.36.0.9" 158 | } 159 | ], 160 | "container_status": { 161 | "network_infos": [ 162 | { 163 | "ip_address": "10.36.0.9", 164 | "ip_addresses": [ 165 | { 166 | "ip_address": "10.36.0.9" 167 | } 168 | ] 169 | } 170 | ] 171 | } 172 | } 173 | ], 174 | "labels": [ 175 | { 176 | "key": "traefik.frontend.value", 177 | "value": "dashing.service.vcloud" 178 | } 179 | ], 180 | "container": { 181 | "type": "DOCKER", 182 | "docker": { 183 | "image": "docker-registry.service.tmp.vcloud:5000/dashing", 184 | "network": "BRIDGE", 185 | "port_mappings": [ 186 | { 187 | "host_port": 31244, 188 | "container_port": 3030, 189 | "protocol": "tcp" 190 | } 191 | ], 192 | "privileged": false, 193 | "force_pull_image": false 194 | } 195 | }, 196 | "framework_name": "marathon", 197 | "hostname": "10.66.1.159", 198 | "timestamp": [ 199 | 1457029435.12229 200 | ] 201 | } 202 | ] 203 | -------------------------------------------------------------------------------- /src/components/TaskVisualizer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "TaskVisualizer", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./TaskVisualizer.js" 6 | } 7 | -------------------------------------------------------------------------------- /src/components/ZookeeperRedirect/ZookeeperRedirect.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | class ZookeeperRedirect extends React.Component { 4 | 5 | static propTypes = { 6 | leader: PropTypes.string.isRequired, 7 | pid: PropTypes.string.isRequired, 8 | redirectTime: PropTypes.number.isRequired 9 | }; 10 | 11 | componentDidMount() { 12 | this.redirectToLeader() 13 | } 14 | 15 | shouldComponentUpdate(nextProps) { 16 | return nextProps.leader !== this.props.leader; 17 | } 18 | 19 | componentDidUpdate() { 20 | this.redirectToLeader() 21 | } 22 | 23 | redirectToLeader() { 24 | if (this.props.leader) { 25 | // Redirect if we aren't the leader. 26 | if (this.props.leader != this.props.pid) { 27 | setTimeout(function () { 28 | window.location = '/master/redirect'; 29 | }, this.props.redirectTime); 30 | } 31 | } 32 | } 33 | 34 | /* @todo - allow overridable styles */ 35 | getStyles() { 36 | let styles = { 37 | fontWeight: 500, 38 | marginBottom: 20 39 | }; 40 | return styles; 41 | } 42 | 43 | createAlert() { 44 | var className = 'hide'; 45 | var alert = null; 46 | 47 | if (this.props.leader) { 48 | if (this.props.leader != this.props.pid) { 49 | className = 'show'; 50 | alert = React.createElement('span', null, 'This master is not the leader, redirecting...'); 51 | } 52 | } 53 | else { 54 | className = 'show'; 55 | alert = React.createElement('span', null, 'No master currently leading...'); 56 | } 57 | return { 'alert': alert, 'className': className } 58 | } 59 | 60 | render() { 61 | 62 | let style = this.getStyles(); 63 | let alert = this.createAlert().alert; 64 | let className = this.createAlert().className; 65 | return ( 66 |
    67 | {alert} 68 |
    69 | ); 70 | } 71 | } 72 | 73 | export default ZookeeperRedirect; 74 | -------------------------------------------------------------------------------- /src/components/ZookeeperRedirect/__tests__/ZookeeperRedirect-test.js: -------------------------------------------------------------------------------- 1 | jest.autoMockOff(); 2 | describe('ZookeeperRedirect', function() { 3 | 4 | var React = require('react/addons'); 5 | var TestUtils = React.addons.TestUtils; 6 | var ZookeeperRedirect = require('../ZookeeperRedirect.js'); 7 | 8 | it('is hidden when leader == pid', function() { 9 | // Render a ZookeeperRedirect in the document. 10 | var zookeeperRedirectRendered = TestUtils.renderIntoDocument( 11 | 12 | ); 13 | 14 | // Verify that it's hide by default. 15 | var zooKeeperDiv = TestUtils.findRenderedDOMComponentWithTag( 16 | zookeeperRedirectRendered, 'div'); 17 | expect(React.findDOMNode(zooKeeperDiv).className).toEqual('hide'); 18 | }); 19 | 20 | it('shows message when leader == null', function() { 21 | 22 | var zookeeperRedirectRendered = TestUtils.renderIntoDocument( 23 | 24 | ); 25 | 26 | var zooKeeperDiv = TestUtils.findRenderedDOMComponentWithTag( 27 | zookeeperRedirectRendered, 'div'); 28 | expect(React.findDOMNode(zooKeeperDiv).className).toEqual('show'); 29 | 30 | var alertSpan = TestUtils.findRenderedDOMComponentWithTag( 31 | zookeeperRedirectRendered, 'span'); 32 | expect(React.findDOMNode(alertSpan).innerHTML).toEqual('No master currently leading...'); 33 | }); 34 | 35 | it('shows message when leader != pid', function() { 36 | 37 | var zookeeperRedirectRendered = TestUtils.renderIntoDocument( 38 | 39 | ); 40 | 41 | var zooKeeperDiv = TestUtils.findRenderedDOMComponentWithTag( 42 | zookeeperRedirectRendered, 'div'); 43 | expect(React.findDOMNode(zooKeeperDiv).className).toEqual('show'); 44 | 45 | var alertSpan = TestUtils.findRenderedDOMComponentWithTag( 46 | zookeeperRedirectRendered, 'span'); 47 | expect(React.findDOMNode(alertSpan).innerHTML).toEqual('This master is not the leader, redirecting...'); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/components/ZookeeperRedirect/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ZookeeperRedirect", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./ZookeeperRedirect.js" 6 | } 7 | -------------------------------------------------------------------------------- /src/config/config.js: -------------------------------------------------------------------------------- 1 | var config = { 2 | // Interval of API request in ms 3 | updateInterval: 8000, 4 | port: 5000, 5 | // Backend API URLs. 6 | // Set this to 'http://127.0.0.1:8000' for using the stub server running in DEV mode. 7 | 'mesosEndpoint': (typeof(document) !== 'undefined' ? document.location.origin : null), 8 | 'proxyPath': '/proxy', 9 | 'zookeeperPath': '/mesos' 10 | }; 11 | 12 | if (process.env.ZOOKEEPER_ADDRESS) { 13 | config.zookeeperAddress = process.env.ZOOKEEPER_ADDRESS; 14 | config.mesosEndpoint = config.proxyPath; 15 | } else if (process.env.MESOS_ENDPOINT) { 16 | config.mesosEndpoint = config.proxyPath; 17 | } 18 | 19 | if (process.env.ZOOKEEPER_PATH) { 20 | config.zookeeperPath = process.env.ZOOKEEPER_PATH; 21 | } 22 | 23 | if (process.env.PORT0) { 24 | config.port = process.env.PORT0; 25 | } 26 | module.exports = config; 27 | -------------------------------------------------------------------------------- /src/constants/ClusterConstants.js: -------------------------------------------------------------------------------- 1 | import keyMirror from 'react/lib/keyMirror'; 2 | 3 | export default keyMirror({ 4 | CLUSTER_REFRESH_STATS: null, 5 | CLUSTER_REFRESH_LOGS: null, 6 | CLUSTER_REFRESH_STATE: null 7 | }); 8 | -------------------------------------------------------------------------------- /src/core/Dispatcher.js: -------------------------------------------------------------------------------- 1 | import { Dispatcher } from 'flux'; 2 | 3 | export default new Dispatcher(); 4 | -------------------------------------------------------------------------------- /src/core/__tests__/Dispatcher-test.js: -------------------------------------------------------------------------------- 1 | 2 | jest.autoMockOff(); 3 | 4 | import React from 'react/addons'; 5 | import Dispatcher from '../Dispatcher'; 6 | 7 | describe('Dispatcher', function() { 8 | var Dispatcher; 9 | 10 | beforeEach(function() { 11 | Dispatcher = require('../Dispatcher'); 12 | }); 13 | 14 | it('sends actions to subscribers', function() { 15 | var listener = jest.genMockFunction(); 16 | Dispatcher.register(listener); 17 | 18 | var payload = {}; 19 | Dispatcher.dispatch(payload); 20 | expect(listener.mock.calls.length).toBe(1); 21 | expect(listener.mock.calls[0][0]).toBe(payload); 22 | }); 23 | 24 | it('waits with chained dependencies properly', function() { 25 | var payload = {}; 26 | 27 | var listener1Done = false; 28 | var listener1 = function(pl) { 29 | Dispatcher.waitFor([index2, index4]); 30 | // Second, third, and fourth listeners should have now been called 31 | expect(listener2Done).toBe(true); 32 | expect(listener3Done).toBe(true); 33 | expect(listener4Done).toBe(true); 34 | listener1Done = true; 35 | }; 36 | var index1 = Dispatcher.register(listener1); 37 | 38 | var listener2Done = false; 39 | var listener2 = function(pl) { 40 | Dispatcher.waitFor([index3]); 41 | expect(listener3Done).toBe(true); 42 | listener2Done = true; 43 | }; 44 | var index2 = Dispatcher.register(listener2); 45 | 46 | var listener3Done = false; 47 | var listener3 = function(pl) { 48 | listener3Done = true; 49 | }; 50 | var index3 = Dispatcher.register(listener3); 51 | 52 | var listener4Done = false; 53 | var listener4 = function(pl) { 54 | Dispatcher.waitFor([index3]); 55 | expect(listener3Done).toBe(true); 56 | listener4Done = true; 57 | }; 58 | var index4 = Dispatcher.register(listener4); 59 | 60 | runs(function() { 61 | Dispatcher.dispatch(payload); 62 | }); 63 | 64 | waitsFor(function() { 65 | return listener1Done; 66 | }, "Not all subscribers were properly called", 500); 67 | 68 | runs(function() { 69 | expect(listener1Done).toBe(true); 70 | expect(listener2Done).toBe(true); 71 | expect(listener3Done).toBe(true); 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /src/decorators/withContext.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext: true */ 2 | 3 | import React, { PropTypes } from 'react'; // eslint-disable-line no-unused-vars 4 | import emptyFunction from '../../node_modules/react/lib/emptyFunction'; 5 | 6 | function withContext(ComposedComponent) { 7 | return class WithContext { 8 | 9 | static propTypes = { 10 | context: PropTypes.shape({ 11 | onSetTitle: PropTypes.func, 12 | onSetMeta: PropTypes.func, 13 | onInsertCss: PropTypes.func 14 | }) 15 | }; 16 | 17 | static childContextTypes = { 18 | onInsertCss: PropTypes.func.isRequired, 19 | onSetTitle: PropTypes.func.isRequired, 20 | onSetMeta: PropTypes.func.isRequired 21 | }; 22 | 23 | getChildContext() { 24 | let context = this.props.context; 25 | return { 26 | onInsertCss: context.onInsertCss || emptyFunction, 27 | onSetTitle: context.onSetTitle || emptyFunction, 28 | onSetMeta: context.onSetMeta || emptyFunction 29 | }; 30 | } 31 | 32 | render() { 33 | let { context, ...other } = this.props; // eslint-disable-line no-unused-vars 34 | return ; 35 | } 36 | 37 | }; 38 | } 39 | 40 | export default withContext; 41 | -------------------------------------------------------------------------------- /src/pages/Dashboard/Dashboard.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import PageTitle from '../../components/PageTitle'; 3 | import DashboardBox from '../../components/DashboardBox'; 4 | import Donut from '../../components/Donut'; 5 | import ClusterStore from '../../stores/ClusterStore'; 6 | import _ from 'lodash'; 7 | 8 | class Dashboard extends React.Component { 9 | 10 | static propTypes = { 11 | nodes: PropTypes.array.isRequired, 12 | frameworks: PropTypes.array.isRequired, 13 | stats: PropTypes.object.isRequired 14 | }; 15 | 16 | static contextTypes = { 17 | onSetTitle: PropTypes.func.isRequired 18 | }; 19 | 20 | cpuStats() { 21 | let stats = this.props.stats; 22 | let cpuFree = stats.cpusTotal - stats.cpusUsed; 23 | return ([ 24 | {name: 'Used', count: _.round(stats.cpusUsed, 2), color: 'deepPurple'}, 25 | {name: 'Free', count: _.round(cpuFree, 2), color: 'grey'} 26 | ]); 27 | } 28 | 29 | memoryStats() { 30 | let stats = this.props.stats; 31 | let memoryFree = stats.memTotal - stats.memUsed; 32 | return ([ 33 | {name: 'Used', count: ClusterStore.convertMBtoGB(stats.memUsed), color: 'cyan'}, 34 | {name: 'Free', count: ClusterStore.convertMBtoGB(memoryFree), color: 'grey'} 35 | ]); 36 | } 37 | 38 | diskStats() { 39 | let stats = this.props.stats; 40 | let diskFree = stats.diskTotal - stats.diskUsed; 41 | return ([ 42 | {name: 'Used', count: ClusterStore.convertMBtoGB(stats.diskUsed), color: 'orange'}, 43 | {name: 'Free', count: ClusterStore.convertMBtoGB(diskFree), color: 'grey'} 44 | ]); 45 | } 46 | 47 | taskStats() { 48 | let stats = this.props.stats; 49 | return ([ 50 | {name: 'Running', count: stats.tasksRunning, color: 'green'}, 51 | {name: 'Staged', count: stats.tasksStaging, color: 'amber'} 52 | ]); 53 | } 54 | 55 | nodeStats() { 56 | let stats = this.props.stats; 57 | return ([ 58 | {name: 'Connected', count: stats.slavesConnected, color: 'green'}, 59 | {name: 'Disconnected', count: stats.slavesDisconnected, color: 'red'} 60 | ]); 61 | } 62 | 63 | render() { 64 | let title = 'Dashboard'; 65 | this.context.onSetTitle(title); 66 | return ( 67 |
    68 | 69 |
    70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 |
    90 |
    91 | ); 92 | } 93 | 94 | } 95 | 96 | export default Dashboard; 97 | -------------------------------------------------------------------------------- /src/pages/Dashboard/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Dashboard", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./Dashboard.js" 6 | } 7 | -------------------------------------------------------------------------------- /src/pages/Frameworks/Frameworks.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import PageTitle from '../../components/PageTitle'; 3 | import DashboardBox from '../../components/DashboardBox'; 4 | import FrameworkBlock from '../../components/FrameworkBlock'; 5 | 6 | class Frameworks extends React.Component { 7 | 8 | static propTypes = { 9 | frameworks: PropTypes.array.isRequired 10 | }; 11 | 12 | static contextTypes = { 13 | onSetTitle: PropTypes.func.isRequired 14 | }; 15 | 16 | render() { 17 | let title = 'Frameworks'; 18 | let frameworks = this.props.frameworks; 19 | this.context.onSetTitle(title); 20 | 21 | let widgets = frameworks.map(function(framework, i){ 22 | return React.createElement(DashboardBox, { styles: { root: { minHeight: 'auto' } }, key: i, title: framework.name, xsColSize: 6, smColSize: 4, mdColSize: 3 }, 23 | React.createElement(FrameworkBlock, { key: i, name: framework.name, url: framework.webuiUrl, tasks: framework.tasks.length }) 24 | ); 25 | }); 26 | 27 | return ( 28 |
    29 | 30 |
    31 | {widgets} 32 |
    33 |
    34 | ); 35 | } 36 | 37 | } 38 | 39 | export default Frameworks; 40 | -------------------------------------------------------------------------------- /src/pages/Frameworks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Frameworks", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./Frameworks.js" 6 | } 7 | -------------------------------------------------------------------------------- /src/pages/Logs/Logs.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import PageTitle from '../../components/PageTitle'; 3 | 4 | class Logs extends React.Component { 5 | 6 | static propTypes = { 7 | logs: PropTypes.object.isRequired 8 | }; 9 | 10 | static contextTypes = { 11 | onSetTitle: PropTypes.func.isRequired 12 | }; 13 | 14 | getStyles() { 15 | 16 | let style = { 17 | logs: { 18 | border: 0, 19 | borderWidth: 0, 20 | overflow: 'auto', 21 | height: '500' 22 | } 23 | }; 24 | return style; 25 | } 26 | render() { 27 | let style = this.getStyles(); 28 | let title = 'Master Logs'; 29 | let logs = this.props.logs; 30 | this.context.onSetTitle(title); 31 | 32 | return ( 33 |
    34 | 35 |
    36 |
    37 |           { logs['data'] }
    38 |           
    39 |
    40 |
    41 | ); 42 | } 43 | 44 | } 45 | 46 | export default Logs; 47 | -------------------------------------------------------------------------------- /src/pages/Logs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Loga", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./Logs.js" 6 | } 7 | -------------------------------------------------------------------------------- /src/pages/Nodes/Nodes.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import PageTitle from '../../components/PageTitle'; 3 | import DashboardBox from '../../components/DashboardBox'; 4 | import Sunburst from '../../components/Sunburst'; 5 | 6 | class Nodes extends React.Component { 7 | 8 | static propTypes = { 9 | nodes: PropTypes.array.isRequired 10 | }; 11 | 12 | static contextTypes = { 13 | onSetTitle: PropTypes.func.isRequired 14 | }; 15 | 16 | render() { 17 | let title = 'Nodes'; 18 | let nodes = this.props.nodes; 19 | 20 | let widgets = nodes.map(function(node, i){ 21 | return React.createElement(DashboardBox, { title: node.hostname }, 22 | React.createElement(Sunburst, { key: i, totalResources: node.resources, usedResources: node.usedResources }) 23 | ); 24 | }); 25 | 26 | this.context.onSetTitle(title); 27 | 28 | return ( 29 |
    30 | 31 |
    32 | { widgets } 33 |
    34 |
    35 | ); 36 | } 37 | 38 | } 39 | 40 | export default Nodes; 41 | -------------------------------------------------------------------------------- /src/pages/Nodes/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Nodes", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./Nodes.js" 6 | } 7 | -------------------------------------------------------------------------------- /src/pages/NotFound/NotFound.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | import PageTitle from '../../components/PageTitle'; 3 | 4 | class NotFound extends React.Component { 5 | 6 | static contextTypes = { 7 | onSetTitle: PropTypes.func.isRequired 8 | }; 9 | 10 | render() { 11 | let title = 'Page Not Found'; 12 | this.context.onSetTitle(title); 13 | return ( 14 |
    15 | 16 |

    Sorry, but the page you were trying to view does not exist.

    17 |
    18 | ); 19 | } 20 | 21 | } 22 | 23 | export default NotFound; 24 | -------------------------------------------------------------------------------- /src/pages/NotFound/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NotFound", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./NotFound.js" 6 | } 7 | -------------------------------------------------------------------------------- /src/pages/Tasks/Tasks.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext: true */ 2 | 3 | import React, { PropTypes } from 'react'; 4 | import PageTitle from '../../components/PageTitle'; 5 | import TaskTable from '../../components/TaskTable'; 6 | import TaskVisualizer from '../../components/TaskVisualizer'; 7 | import Tabs from 'material-ui/lib/tabs/tabs'; 8 | import Tab from 'material-ui/lib/tabs/tab'; 9 | 10 | class Tasks extends React.Component { 11 | 12 | static propTypes = { 13 | tasks: PropTypes.array.isRequired 14 | }; 15 | 16 | static contextTypes = { 17 | onSetTitle: PropTypes.func.isRequired 18 | }; 19 | 20 | render() { 21 | const styles = { 22 | headline: { 23 | fontSize: 24, 24 | paddingTop: 16, 25 | marginBottom: 12, 26 | fontWeight: 400, 27 | }, 28 | }; 29 | 30 | let title = 'Tasks'; 31 | this.context.onSetTitle(title); 32 | return ( 33 |
    34 | 35 | 36 | 37 |
    38 | 39 |
    40 |
    41 | 42 |
    43 | 44 |
    45 |
    46 |
    47 |
    48 | ); 49 | } 50 | } 51 | 52 | export default Tasks; 53 | -------------------------------------------------------------------------------- /src/pages/Tasks/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Tasks", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "./Tasks.js" 6 | } 7 | -------------------------------------------------------------------------------- /src/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Capgemini/mesos-ui/b45798230528ac176820661696a770bf97903a7a/src/public/apple-touch-icon.png -------------------------------------------------------------------------------- /src/public/assets/icon-framework-chronos.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Capgemini/mesos-ui/b45798230528ac176820661696a770bf97903a7a/src/public/assets/icon-framework-chronos.png -------------------------------------------------------------------------------- /src/public/assets/icon-framework-jenkins.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Capgemini/mesos-ui/b45798230528ac176820661696a770bf97903a7a/src/public/assets/icon-framework-jenkins.png -------------------------------------------------------------------------------- /src/public/assets/icon-framework-marathon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Capgemini/mesos-ui/b45798230528ac176820661696a770bf97903a7a/src/public/assets/icon-framework-marathon.png -------------------------------------------------------------------------------- /src/public/assets/icon-framework-myriad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Capgemini/mesos-ui/b45798230528ac176820661696a770bf97903a7a/src/public/assets/icon-framework-myriad.png -------------------------------------------------------------------------------- /src/public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/public/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /src/public/css/flexboxgrid.min.css: -------------------------------------------------------------------------------- 1 | .container-fluid{margin-right:auto;margin-left:auto;padding-right:2rem;padding-left:2rem}.row{box-sizing:border-box;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-box-flex:0;-webkit-flex:0 1 auto;-ms-flex:0 1 auto;flex:0 1 auto;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-webkit-flex-direction:row;-ms-flex-direction:row;flex-direction:row;-webkit-flex-wrap:wrap;-ms-flex-wrap:wrap;flex-wrap:wrap;margin-right:-.5rem;margin-left:-.5rem}.row.reverse{-webkit-box-orient:horizontal;-webkit-box-direction:reverse;-webkit-flex-direction:row-reverse;-ms-flex-direction:row-reverse;flex-direction:row-reverse}.col.reverse{-webkit-box-orient:vertical;-webkit-box-direction:reverse;-webkit-flex-direction:column-reverse;-ms-flex-direction:column-reverse;flex-direction:column-reverse}.col-xs,.col-xs-1,.col-xs-10,.col-xs-11,.col-xs-12,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-offset-1,.col-xs-offset-10,.col-xs-offset-11,.col-xs-offset-12,.col-xs-offset-2,.col-xs-offset-3,.col-xs-offset-4,.col-xs-offset-5,.col-xs-offset-6,.col-xs-offset-7,.col-xs-offset-8,.col-xs-offset-9{box-sizing:border-box;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex:0 1 auto;-ms-flex:0 1 auto;flex:0 1 auto;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-box-flex:0;-webkit-flex-grow:0;-ms-flex-positive:0;flex-grow:0;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;padding-right:.5rem;padding-left:.5rem}.col-xs{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;max-width:100%}.col-xs-1{-webkit-flex-basis:8.333333333%;-ms-flex-preferred-size:8.333333333%;flex-basis:8.333333333%;max-width:8.333333333%}.col-xs-2{-webkit-flex-basis:16.666666667%;-ms-flex-preferred-size:16.666666667%;flex-basis:16.666666667%;max-width:16.666666667%}.col-xs-3{-webkit-flex-basis:25%;-ms-flex-preferred-size:25%;flex-basis:25%;max-width:25%}.col-xs-4{-webkit-flex-basis:33.333333333%;-ms-flex-preferred-size:33.333333333%;flex-basis:33.333333333%;max-width:33.333333333%}.col-xs-5{-webkit-flex-basis:41.666666667%;-ms-flex-preferred-size:41.666666667%;flex-basis:41.666666667%;max-width:41.666666667%}.col-xs-6{-webkit-flex-basis:50%;-ms-flex-preferred-size:50%;flex-basis:50%;max-width:50%}.col-xs-7{-webkit-flex-basis:58.333333333%;-ms-flex-preferred-size:58.333333333%;flex-basis:58.333333333%;max-width:58.333333333%}.col-xs-8{-webkit-flex-basis:66.666666667%;-ms-flex-preferred-size:66.666666667%;flex-basis:66.666666667%;max-width:66.666666667%}.col-xs-9{-webkit-flex-basis:75%;-ms-flex-preferred-size:75%;flex-basis:75%;max-width:75%}.col-xs-10{-webkit-flex-basis:83.333333333%;-ms-flex-preferred-size:83.333333333%;flex-basis:83.333333333%;max-width:83.333333333%}.col-xs-11{-webkit-flex-basis:91.666666667%;-ms-flex-preferred-size:91.666666667%;flex-basis:91.666666667%;max-width:91.666666667%}.col-xs-12{-webkit-flex-basis:100%;-ms-flex-preferred-size:100%;flex-basis:100%;max-width:100%}.col-xs-offset-1{margin-left:8.333333333%}.col-xs-offset-2{margin-left:16.666666667%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-4{margin-left:33.333333333%}.col-xs-offset-5{margin-left:41.666666667%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-7{margin-left:58.333333333%}.col-xs-offset-8{margin-left:66.666666667%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-10{margin-left:83.333333333%}.col-xs-offset-11{margin-left:91.666666667%}.start-xs{-webkit-box-pack:start;-webkit-justify-content:flex-start;-ms-flex-pack:start;justify-content:flex-start;text-align:start}.center-xs{-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;text-align:center}.end-xs{-webkit-box-pack:end;-webkit-justify-content:flex-end;-ms-flex-pack:end;justify-content:flex-end;text-align:end}.top-xs{-webkit-box-align:start;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start}.middle-xs{-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.bottom-xs{-webkit-box-align:end;-webkit-align-items:flex-end;-ms-flex-align:end;align-items:flex-end}.around-xs{-webkit-justify-content:space-around;-ms-flex-pack:distribute;justify-content:space-around}.between-xs{-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between}.first-xs{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.last-xs{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}@media only screen and (min-width:48em){.container{width:46rem}.col-sm,.col-sm-1,.col-sm-10,.col-sm-11,.col-sm-12,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-offset-1,.col-sm-offset-10,.col-sm-offset-11,.col-sm-offset-12,.col-sm-offset-2,.col-sm-offset-3,.col-sm-offset-4,.col-sm-offset-5,.col-sm-offset-6,.col-sm-offset-7,.col-sm-offset-8,.col-sm-offset-9{box-sizing:border-box;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex:0 1 auto;-ms-flex:0 1 auto;flex:0 1 auto;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-box-flex:0;-webkit-flex-grow:0;-ms-flex-positive:0;flex-grow:0;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;padding-right:.5rem;padding-left:.5rem}.col-sm{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;max-width:100%}.col-sm-1{-webkit-flex-basis:8.333333333%;-ms-flex-preferred-size:8.333333333%;flex-basis:8.333333333%;max-width:8.333333333%}.col-sm-2{-webkit-flex-basis:16.666666667%;-ms-flex-preferred-size:16.666666667%;flex-basis:16.666666667%;max-width:16.666666667%}.col-sm-3{-webkit-flex-basis:25%;-ms-flex-preferred-size:25%;flex-basis:25%;max-width:25%}.col-sm-4{-webkit-flex-basis:33.333333333%;-ms-flex-preferred-size:33.333333333%;flex-basis:33.333333333%;max-width:33.333333333%}.col-sm-5{-webkit-flex-basis:41.666666667%;-ms-flex-preferred-size:41.666666667%;flex-basis:41.666666667%;max-width:41.666666667%}.col-sm-6{-webkit-flex-basis:50%;-ms-flex-preferred-size:50%;flex-basis:50%;max-width:50%}.col-sm-7{-webkit-flex-basis:58.333333333%;-ms-flex-preferred-size:58.333333333%;flex-basis:58.333333333%;max-width:58.333333333%}.col-sm-8{-webkit-flex-basis:66.666666667%;-ms-flex-preferred-size:66.666666667%;flex-basis:66.666666667%;max-width:66.666666667%}.col-sm-9{-webkit-flex-basis:75%;-ms-flex-preferred-size:75%;flex-basis:75%;max-width:75%}.col-sm-10{-webkit-flex-basis:83.333333333%;-ms-flex-preferred-size:83.333333333%;flex-basis:83.333333333%;max-width:83.333333333%}.col-sm-11{-webkit-flex-basis:91.666666667%;-ms-flex-preferred-size:91.666666667%;flex-basis:91.666666667%;max-width:91.666666667%}.col-sm-12{-webkit-flex-basis:100%;-ms-flex-preferred-size:100%;flex-basis:100%;max-width:100%}.col-sm-offset-1{margin-left:8.333333333%}.col-sm-offset-2{margin-left:16.666666667%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-4{margin-left:33.333333333%}.col-sm-offset-5{margin-left:41.666666667%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-7{margin-left:58.333333333%}.col-sm-offset-8{margin-left:66.666666667%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-10{margin-left:83.333333333%}.col-sm-offset-11{margin-left:91.666666667%}.start-sm{-webkit-box-pack:start;-webkit-justify-content:flex-start;-ms-flex-pack:start;justify-content:flex-start;text-align:start}.center-sm{-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;text-align:center}.end-sm{-webkit-box-pack:end;-webkit-justify-content:flex-end;-ms-flex-pack:end;justify-content:flex-end;text-align:end}.top-sm{-webkit-box-align:start;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start}.middle-sm{-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.bottom-sm{-webkit-box-align:end;-webkit-align-items:flex-end;-ms-flex-align:end;align-items:flex-end}.around-sm{-webkit-justify-content:space-around;-ms-flex-pack:distribute;justify-content:space-around}.between-sm{-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between}.first-sm{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.last-sm{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}}@media only screen and (min-width:62em){.container{width:61rem}.col-md,.col-md-1,.col-md-10,.col-md-11,.col-md-12,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-offset-1,.col-md-offset-10,.col-md-offset-11,.col-md-offset-12,.col-md-offset-2,.col-md-offset-3,.col-md-offset-4,.col-md-offset-5,.col-md-offset-6,.col-md-offset-7,.col-md-offset-8,.col-md-offset-9{box-sizing:border-box;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex:0 1 auto;-ms-flex:0 1 auto;flex:0 1 auto;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-box-flex:0;-webkit-flex-grow:0;-ms-flex-positive:0;flex-grow:0;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;padding-right:.5rem;padding-left:.5rem}.col-md{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;max-width:100%}.col-md-1{-webkit-flex-basis:8.333333333%;-ms-flex-preferred-size:8.333333333%;flex-basis:8.333333333%;max-width:8.333333333%}.col-md-2{-webkit-flex-basis:16.666666667%;-ms-flex-preferred-size:16.666666667%;flex-basis:16.666666667%;max-width:16.666666667%}.col-md-3{-webkit-flex-basis:25%;-ms-flex-preferred-size:25%;flex-basis:25%;max-width:25%}.col-md-4{-webkit-flex-basis:33.333333333%;-ms-flex-preferred-size:33.333333333%;flex-basis:33.333333333%;max-width:33.333333333%}.col-md-5{-webkit-flex-basis:41.666666667%;-ms-flex-preferred-size:41.666666667%;flex-basis:41.666666667%;max-width:41.666666667%}.col-md-6{-webkit-flex-basis:50%;-ms-flex-preferred-size:50%;flex-basis:50%;max-width:50%}.col-md-7{-webkit-flex-basis:58.333333333%;-ms-flex-preferred-size:58.333333333%;flex-basis:58.333333333%;max-width:58.333333333%}.col-md-8{-webkit-flex-basis:66.666666667%;-ms-flex-preferred-size:66.666666667%;flex-basis:66.666666667%;max-width:66.666666667%}.col-md-9{-webkit-flex-basis:75%;-ms-flex-preferred-size:75%;flex-basis:75%;max-width:75%}.col-md-10{-webkit-flex-basis:83.333333333%;-ms-flex-preferred-size:83.333333333%;flex-basis:83.333333333%;max-width:83.333333333%}.col-md-11{-webkit-flex-basis:91.666666667%;-ms-flex-preferred-size:91.666666667%;flex-basis:91.666666667%;max-width:91.666666667%}.col-md-12{-webkit-flex-basis:100%;-ms-flex-preferred-size:100%;flex-basis:100%;max-width:100%}.col-md-offset-1{margin-left:8.333333333%}.col-md-offset-2{margin-left:16.666666667%}.col-md-offset-3{margin-left:25%}.col-md-offset-4{margin-left:33.333333333%}.col-md-offset-5{margin-left:41.666666667%}.col-md-offset-6{margin-left:50%}.col-md-offset-7{margin-left:58.333333333%}.col-md-offset-8{margin-left:66.666666667%}.col-md-offset-9{margin-left:75%}.col-md-offset-10{margin-left:83.333333333%}.col-md-offset-11{margin-left:91.666666667%}.start-md{-webkit-box-pack:start;-webkit-justify-content:flex-start;-ms-flex-pack:start;justify-content:flex-start;text-align:start}.center-md{-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;text-align:center}.end-md{-webkit-box-pack:end;-webkit-justify-content:flex-end;-ms-flex-pack:end;justify-content:flex-end;text-align:end}.top-md{-webkit-box-align:start;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start}.middle-md{-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.bottom-md{-webkit-box-align:end;-webkit-align-items:flex-end;-ms-flex-align:end;align-items:flex-end}.around-md{-webkit-justify-content:space-around;-ms-flex-pack:distribute;justify-content:space-around}.between-md{-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between}.first-md{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.last-md{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}}@media only screen and (min-width:75em){.container{width:71rem}.col-lg,.col-lg-1,.col-lg-10,.col-lg-11,.col-lg-12,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-offset-1,.col-lg-offset-10,.col-lg-offset-11,.col-lg-offset-12,.col-lg-offset-2,.col-lg-offset-3,.col-lg-offset-4,.col-lg-offset-5,.col-lg-offset-6,.col-lg-offset-7,.col-lg-offset-8,.col-lg-offset-9{box-sizing:border-box;display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex;-webkit-flex:0 1 auto;-ms-flex:0 1 auto;flex:0 1 auto;-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column;-webkit-box-flex:0;-webkit-flex-grow:0;-ms-flex-positive:0;flex-grow:0;-webkit-flex-shrink:0;-ms-flex-negative:0;flex-shrink:0;padding-right:.5rem;padding-left:.5rem}.col-lg{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1;-webkit-flex-basis:0;-ms-flex-preferred-size:0;flex-basis:0;max-width:100%}.col-lg-1{-webkit-flex-basis:8.333333333%;-ms-flex-preferred-size:8.333333333%;flex-basis:8.333333333%;max-width:8.333333333%}.col-lg-2{-webkit-flex-basis:16.666666667%;-ms-flex-preferred-size:16.666666667%;flex-basis:16.666666667%;max-width:16.666666667%}.col-lg-3{-webkit-flex-basis:25%;-ms-flex-preferred-size:25%;flex-basis:25%;max-width:25%}.col-lg-4{-webkit-flex-basis:33.333333333%;-ms-flex-preferred-size:33.333333333%;flex-basis:33.333333333%;max-width:33.333333333%}.col-lg-5{-webkit-flex-basis:41.666666667%;-ms-flex-preferred-size:41.666666667%;flex-basis:41.666666667%;max-width:41.666666667%}.col-lg-6{-webkit-flex-basis:50%;-ms-flex-preferred-size:50%;flex-basis:50%;max-width:50%}.col-lg-7{-webkit-flex-basis:58.333333333%;-ms-flex-preferred-size:58.333333333%;flex-basis:58.333333333%;max-width:58.333333333%}.col-lg-8{-webkit-flex-basis:66.666666667%;-ms-flex-preferred-size:66.666666667%;flex-basis:66.666666667%;max-width:66.666666667%}.col-lg-9{-webkit-flex-basis:75%;-ms-flex-preferred-size:75%;flex-basis:75%;max-width:75%}.col-lg-10{-webkit-flex-basis:83.333333333%;-ms-flex-preferred-size:83.333333333%;flex-basis:83.333333333%;max-width:83.333333333%}.col-lg-11{-webkit-flex-basis:91.666666667%;-ms-flex-preferred-size:91.666666667%;flex-basis:91.666666667%;max-width:91.666666667%}.col-lg-12{-webkit-flex-basis:100%;-ms-flex-preferred-size:100%;flex-basis:100%;max-width:100%}.col-lg-offset-1{margin-left:8.333333333%}.col-lg-offset-2{margin-left:16.666666667%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-4{margin-left:33.333333333%}.col-lg-offset-5{margin-left:41.666666667%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-7{margin-left:58.333333333%}.col-lg-offset-8{margin-left:66.666666667%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-10{margin-left:83.333333333%}.col-lg-offset-11{margin-left:91.666666667%}.start-lg{-webkit-box-pack:start;-webkit-justify-content:flex-start;-ms-flex-pack:start;justify-content:flex-start;text-align:start}.center-lg{-webkit-box-pack:center;-webkit-justify-content:center;-ms-flex-pack:center;justify-content:center;text-align:center}.end-lg{-webkit-box-pack:end;-webkit-justify-content:flex-end;-ms-flex-pack:end;justify-content:flex-end;text-align:end}.top-lg{-webkit-box-align:start;-webkit-align-items:flex-start;-ms-flex-align:start;align-items:flex-start}.middle-lg{-webkit-box-align:center;-webkit-align-items:center;-ms-flex-align:center;align-items:center}.bottom-lg{-webkit-box-align:end;-webkit-align-items:flex-end;-ms-flex-align:end;align-items:flex-end}.around-lg{-webkit-justify-content:space-around;-ms-flex-pack:distribute;justify-content:space-around}.between-lg{-webkit-box-pack:justify;-webkit-justify-content:space-between;-ms-flex-pack:justify;justify-content:space-between}.first-lg{-webkit-box-ordinal-group:0;-webkit-order:-1;-ms-flex-order:-1;order:-1}.last-lg{-webkit-box-ordinal-group:2;-webkit-order:1;-ms-flex-order:1;order:1}} -------------------------------------------------------------------------------- /src/public/css/font-icons/material-icons/MaterialIcons-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Capgemini/mesos-ui/b45798230528ac176820661696a770bf97903a7a/src/public/css/font-icons/material-icons/MaterialIcons-Regular.eot -------------------------------------------------------------------------------- /src/public/css/font-icons/material-icons/MaterialIcons-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Capgemini/mesos-ui/b45798230528ac176820661696a770bf97903a7a/src/public/css/font-icons/material-icons/MaterialIcons-Regular.ttf -------------------------------------------------------------------------------- /src/public/css/font-icons/material-icons/MaterialIcons-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Capgemini/mesos-ui/b45798230528ac176820661696a770bf97903a7a/src/public/css/font-icons/material-icons/MaterialIcons-Regular.woff -------------------------------------------------------------------------------- /src/public/css/font-icons/material-icons/MaterialIcons-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Capgemini/mesos-ui/b45798230528ac176820661696a770bf97903a7a/src/public/css/font-icons/material-icons/MaterialIcons-Regular.woff2 -------------------------------------------------------------------------------- /src/public/css/font-icons/material-icons/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Material Icons'; 3 | font-style: normal; 4 | font-weight: 400; 5 | src: url('MaterialIcons-Regular.eot'); /* For IE6-8 */ 6 | src: local('Material Icons'), 7 | local('MaterialIcons-Regular'), 8 | url('MaterialIcons-Regular.woff2') format('woff2'), 9 | url('MaterialIcons-Regular.woff') format('woff'), 10 | url('MaterialIcons-Regular.ttf') format('truetype'); 11 | } 12 | 13 | .material-icons { 14 | font-family: 'Material Icons'; 15 | font-weight: normal; 16 | font-style: normal; 17 | font-size: 24px; /* Preferred icon size */ 18 | display: inline-block; 19 | width: 1em; 20 | height: 1em; 21 | line-height: 1; 22 | text-transform: none; 23 | letter-spacing: normal; 24 | word-wrap: normal; 25 | white-space: nowrap; 26 | direction: ltr; 27 | 28 | /* Support for all WebKit browsers. */ 29 | -webkit-font-smoothing: antialiased; 30 | /* Support for Safari and Chrome. */ 31 | text-rendering: optimizeLegibility; 32 | 33 | /* Support for Firefox. */ 34 | -moz-osx-font-smoothing: grayscale; 35 | 36 | /* Support for IE. */ 37 | font-feature-settings: 'liga'; 38 | } 39 | -------------------------------------------------------------------------------- /src/public/css/font-icons/material-ui-icons/icomoon.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Capgemini/mesos-ui/b45798230528ac176820661696a770bf97903a7a/src/public/css/font-icons/material-ui-icons/icomoon.eot -------------------------------------------------------------------------------- /src/public/css/font-icons/material-ui-icons/icomoon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/public/css/font-icons/material-ui-icons/icomoon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Capgemini/mesos-ui/b45798230528ac176820661696a770bf97903a7a/src/public/css/font-icons/material-ui-icons/icomoon.ttf -------------------------------------------------------------------------------- /src/public/css/font-icons/material-ui-icons/icomoon.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Capgemini/mesos-ui/b45798230528ac176820661696a770bf97903a7a/src/public/css/font-icons/material-ui-icons/icomoon.woff -------------------------------------------------------------------------------- /src/public/css/font-icons/material-ui-icons/material-icons.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Capgemini/mesos-ui/b45798230528ac176820661696a770bf97903a7a/src/public/css/font-icons/material-ui-icons/material-icons.woff2 -------------------------------------------------------------------------------- /src/public/css/font-icons/material-ui-icons/material-ui-icons.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Capgemini/mesos-ui/b45798230528ac176820661696a770bf97903a7a/src/public/css/font-icons/material-ui-icons/material-ui-icons.eot -------------------------------------------------------------------------------- /src/public/css/font-icons/material-ui-icons/material-ui-icons.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generated by IcoMoon 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/public/css/font-icons/material-ui-icons/material-ui-icons.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Capgemini/mesos-ui/b45798230528ac176820661696a770bf97903a7a/src/public/css/font-icons/material-ui-icons/material-ui-icons.ttf -------------------------------------------------------------------------------- /src/public/css/font-icons/material-ui-icons/material-ui-icons.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Capgemini/mesos-ui/b45798230528ac176820661696a770bf97903a7a/src/public/css/font-icons/material-ui-icons/material-ui-icons.woff -------------------------------------------------------------------------------- /src/public/css/font-icons/material-ui-icons/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'material-ui-icons'; 3 | src: url('material-ui-icons.eot'); 4 | } 5 | @font-face { 6 | font-family: 'material-ui-icons'; 7 | src: url(data:application/x-font-ttf;charset=utf-8;base64,AAEAAAALAIAAAwAwT1MvMghi/NsAAAC8AAAAYGNtYXDMfszDAAABHAAAAGRnYXNwAAAAEAAAAYAAAAAIZ2x5Zp6RlyoAAAGIAAAELGhlYWQDHAqpAAAFtAAAADZoaGVhA+IB8AAABewAAAAkaG10eBcAAroAAAYQAAAAPGxvY2EFugcGAAAGTAAAACBtYXhwABUAUgAABmwAAAAgbmFtZT0DC0MAAAaMAAABn3Bvc3QAAwAAAAAILAAAACAAAwIAAZAABQAAAUwBZgAAAEcBTAFmAAAA9QAZAIQAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADmJQHg/+AAIAHgACAAAAABAAAAAAAAAAAAAAAgAAAAAAACAAAAAwAAABQAAwABAAAAFAAEAFAAAAAQABAAAwAAAAEAIOYH5gvmEOYl//3//wAAAAAAIOYA5gvmEOYl//3//wAB/+MaBBoBGf0Z6QADAAEAAAAAAAAAAAAAAAAAAAAAAAEAAf//AA8AAQAAAAAAAAAAAAIAADc5AQAAAAABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAABQBAABUBwAGVAAkADgATABcAGwAAEzMnBzMVIxc3IzczFSM1NTMVIzU3FzM1AzczFatAVlVAQFVWQCrr6+vrJyqaxCqaAUBVVdVWVlUrK1UqKlYrK/7VKysAAAABAEAAFQHAAZUAJAAANx4BFzc+ARceATMyFh0BFAYjIi4CNTQ2OwEyFhUUFhcWBg8BjRdJLS8EDAYRJxQIDQ0IS4ViOQ0ISwkMBwUCAgUv7y1IFy4FAwIGBwwJSgkNOWOESwkMDAkUJhIGCwUvAAAAAwAAAGsCAAFVABoAJwA0AAABIgYVFBYXIz4BNTQmIyIGFRQWMyEyNjU0JiMFIiY1NDYzMhYVFAYjISImNTQ2MzIWFRQGIwGLMUUPDGAMD0UxMEVFMAEWMEVFMP7qHysrHx8sLB8BFh8sLB8fKysfAVVEMRUmEBAmFTFERDExREQxMUTALB8fLCwfHywsHx8sLB8fLAAAAAABAIAAgAGAAR4ABQAAAQcnBxc3AWJiYh6AgAEeYmIegIAAAAABACsAFQHVAasACQAAJRcnNy8BDwEXBwEAhCN0mTw8mXQjZVCWZQ2Ojg1llgAAAAABACsAKwHVAZUACgAANzUzFTM1MycHMxXVVmpA1dVAK4CAqsDAqgAAAgArAAAB1QGrABQAHwAAASIOAhUUHgIzMj4CNTQuAiMTJwc3Jz8BHwEHFwEALE46ISE6TiwsTjohITpOLFpaWhhQaSkpaVAYAasiOk4sLE45IiI5TiwsTjoi/qo3N2dFCWFhCUVnAAACABUAFQHrAcAABAAiAAA3MxEjESU0JisBNzU0Ji8BBw4BHQEUFjsBMjY/AT4BPQEjNxVWVgHWGRKHFQUEF4wGBxkSwA0VBUEBAgEBFQEA/wDrEhlhBwcLBRaNBRAJ1RIZDwuXAwgEKQIAAAABAMsAawE1AUAAAgAAPwEny2pqa2prAAACAC0AAgHUAaoABwATAAABFTMuAycHDgEVFBYzMjY3IzUBANQEJTpJKCtQWG5PSGoIzwGq1ShKOSUFMAlrSE9tWVDPAAEALAAHAdQBpABPAAABIg4CFRQWFxY2NTwBNQYmMS4BMSY2MR4BMRY2Nz4BNy4BNTQ2Ny4BNzAWFz4BMzIWFz4BMRYGBx4BFRQGBx4BFRwBFRQWNz4BNTQuAiMBACxNOiFSPwgGLBsIEA4PEBEOJwkBCAQjPQsKAQUIHB8NGg4OGg0fGwkFAQoLPSQGCQYIP1IhOk0sAaQhOk0sRm4VAggEBBYNCSITDAoDARQZAwQKDgQEKD0SHAsEHhYBFAMEBAMUARYeBAscEj0oBAUTDxUgBQQIAhVuRixNOiEAAAAAAQAAAAEAAJWo+pJfDzz1AAsCAAAAAADQ/uT2AAAAAND+5PYAAAAAAgABwAAAAAgAAgAAAAAAAAABAAAB4P/gAAACAAAAAAACAAABAAAAAAAAAAAAAAAAAAAADwAAAAAAAAAAAAAAAAEAAAACAABAAgAAQAIAAAACAACAAgAAKwIAACsCAAArAgAAFQIAAMsCAAAtAgAALAAAAAAACgAUAB4ATACEANAA4gD6AQ4BQgF4AYQBpgIWAAEAAAAPAFAABQAAAAAAAgAAAAAAAAAAAAAAAAAAAAAAAAAOAK4AAQAAAAAAAQAiAAAAAQAAAAAAAgAOAI0AAQAAAAAAAwAiADgAAQAAAAAABAAiAJsAAQAAAAAABQAWACIAAQAAAAAABgARAFoAAQAAAAAACgA0AL0AAwABBAkAAQAiAAAAAwABBAkAAgAOAI0AAwABBAkAAwAiADgAAwABBAkABAAiAJsAAwABBAkABQAWACIAAwABBAkABgAiAGsAAwABBAkACgA0AL0AbQBhAHQAZQByAGkAYQBsAC0AdQBpAC0AaQBjAG8AbgBzAFYAZQByAHMAaQBvAG4AIAAxAC4AMABtAGEAdABlAHIAaQBhAGwALQB1AGkALQBpAGMAbwBuAHNtYXRlcmlhbC11aS1pY29ucwBtAGEAdABlAHIAaQBhAGwALQB1AGkALQBpAGMAbwBuAHMAUgBlAGcAdQBsAGEAcgBtAGEAdABlAHIAaQBhAGwALQB1AGkALQBpAGMAbwBuAHMARgBvAG4AdAAgAGcAZQBuAGUAcgBhAHQAZQBkACAAYgB5ACAASQBjAG8ATQBvAG8AbgAuAAADAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA) format('truetype'), 8 | url(data:application/font-woff;charset=utf-8;base64,d09GRgABAAAAAAiYAAsAAAAACEwAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAABPUy8yAAABCAAAAGAAAABgCGL822NtYXAAAAFoAAAAZAAAAGTMfszDZ2FzcAAAAcwAAAAIAAAACAAAABBnbHlmAAAB1AAABCwAAAQsnpGXKmhlYWQAAAYAAAAANgAAADYDHAqpaGhlYQAABjgAAAAkAAAAJAPiAfBobXR4AAAGXAAAADwAAAA8FwACumxvY2EAAAaYAAAAIAAAACAFugcGbWF4cAAABrgAAAAgAAAAIAAVAFJuYW1lAAAG2AAAAZ8AAAGfPQMLQ3Bvc3QAAAh4AAAAIAAAACAAAwAAAAMCAAGQAAUAAAFMAWYAAABHAUwBZgAAAPUAGQCEAAAAAAAAAAAAAAAAAAAAARAAAAAAAAAAAAAAAAAAAAAAQAAA5iUB4P/gACAB4AAgAAAAAQAAAAAAAAAAAAAAIAAAAAAAAgAAAAMAAAAUAAMAAQAAABQABABQAAAAEAAQAAMAAAABACDmB+YL5hDmJf/9//8AAAAAACDmAOYL5hDmJf/9//8AAf/jGgQaARn9GekAAwABAAAAAAAAAAAAAAAAAAAAAAABAAH//wAPAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAAAAAAAAAAAAAgAANzkBAAAAAAUAQAAVAcABlQAJAA4AEwAXABsAABMzJwczFSMXNyM3MxUjNTUzFSM1NxczNQM3MxWrQFZVQEBVVkAq6+vr6ycqmsQqmgFAVVXVVlZVKytVKipWKyv+1SsrAAAAAQBAABUBwAGVACQAADceARc3PgEXHgEzMhYdARQGIyIuAjU0NjsBMhYVFBYXFgYPAY0XSS0vBAwGEScUCA0NCEuFYjkNCEsJDAcFAgIFL+8tSBcuBQMCBgcMCUoJDTljhEsJDAwJFCYSBgsFLwAAAAMAAABrAgABVQAaACcANAAAASIGFRQWFyM+ATU0JiMiBhUUFjMhMjY1NCYjBSImNTQ2MzIWFRQGIyEiJjU0NjMyFhUUBiMBizFFDwxgDA9FMTBFRTABFjBFRTD+6h8rKx8fLCwfARYfLCwfHysrHwFVRDEVJhAQJhUxREQxMUREMTFEwCwfHywsHx8sLB8fLCwfHywAAAAAAQCAAIABgAEeAAUAAAEHJwcXNwFiYmIegIABHmJiHoCAAAAAAQArABUB1QGrAAkAACUXJzcvAQ8BFwcBAIQjdJk8PJl0I2VQlmUNjo4NZZYAAAAAAQArACsB1QGVAAoAADc1MxUzNTMnBzMV1VZqQNXVQCuAgKrAwKoAAAIAKwAAAdUBqwAUAB8AAAEiDgIVFB4CMzI+AjU0LgIjEycHNyc/AR8BBxcBACxOOiEhOk4sLE46ISE6TixaWloYUGkpKWlQGAGrIjpOLCxOOSIiOU4sLE46Iv6qNzdnRQlhYQlFZwAAAgAVABUB6wHAAAQAIgAANzMRIxElNCYrATc1NCYvAQcOAR0BFBY7ATI2PwE+AT0BIzcVVlYB1hkShxUFBBeMBgcZEsANFQVBAQIBARUBAP8A6xIZYQcHCwUWjQUQCdUSGQ8LlwMIBCkCAAAAAQDLAGsBNQFAAAIAAD8BJ8tqamtqawAAAgAtAAIB1AGqAAcAEwAAARUzLgMnBw4BFRQWMzI2NyM1AQDUBCU6SSgrUFhuT0hqCM8BqtUoSjklBTAJa0hPbVlQzwABACwABwHUAaQATwAAASIOAhUUFhcWNjU8ATUGJjEuATEmNjEeATEWNjc+ATcuATU0NjcuATcwFhc+ATMyFhc+ATEWBgceARUUBgceARUcARUUFjc+ATU0LgIjAQAsTTohUj8IBiwbCBAODxARDicJAQgEIz0LCgEFCBwfDRoODhoNHxsJBQEKCz0kBgkGCD9SITpNLAGkITpNLEZuFQIIBAQWDQkiEwwKAwEUGQMECg4EBCg9EhwLBB4WARQDBAQDFAEWHgQLHBI9KAQFEw8VIAUECAIVbkYsTTohAAAAAAEAAAABAACVqPqSXw889QALAgAAAAAA0P7k9gAAAADQ/uT2AAAAAAIAAcAAAAAIAAIAAAAAAAAAAQAAAeD/4AAAAgAAAAAAAgAAAQAAAAAAAAAAAAAAAAAAAA8AAAAAAAAAAAAAAAABAAAAAgAAQAIAAEACAAAAAgAAgAIAACsCAAArAgAAKwIAABUCAADLAgAALQIAACwAAAAAAAoAFAAeAEwAhADQAOIA+gEOAUIBeAGEAaYCFgABAAAADwBQAAUAAAAAAAIAAAAAAAAAAAAAAAAAAAAAAAAADgCuAAEAAAAAAAEAIgAAAAEAAAAAAAIADgCNAAEAAAAAAAMAIgA4AAEAAAAAAAQAIgCbAAEAAAAAAAUAFgAiAAEAAAAAAAYAEQBaAAEAAAAAAAoANAC9AAMAAQQJAAEAIgAAAAMAAQQJAAIADgCNAAMAAQQJAAMAIgA4AAMAAQQJAAQAIgCbAAMAAQQJAAUAFgAiAAMAAQQJAAYAIgBrAAMAAQQJAAoANAC9AG0AYQB0AGUAcgBpAGEAbAAtAHUAaQAtAGkAYwBvAG4AcwBWAGUAcgBzAGkAbwBuACAAMQAuADAAbQBhAHQAZQByAGkAYQBsAC0AdQBpAC0AaQBjAG8AbgBzbWF0ZXJpYWwtdWktaWNvbnMAbQBhAHQAZQByAGkAYQBsAC0AdQBpAC0AaQBjAG8AbgBzAFIAZQBnAHUAbABhAHIAbQBhAHQAZQByAGkAYQBsAC0AdQBpAC0AaQBjAG8AbgBzAEYAbwBuAHQAIABnAGUAbgBlAHIAYQB0AGUAZAAgAGIAeQAgAEkAYwBvAE0AbwBvAG4ALgAAAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==) format('woff'); 9 | font-weight: normal; 10 | font-style: normal; 11 | } 12 | 13 | [class^="muidocs-icon-"], [class*=" muidocs-icon-"] { 14 | font-family: 'material-ui-icons'; 15 | speak: none; 16 | font-style: normal; 17 | font-weight: normal; 18 | font-variant: normal; 19 | text-transform: none; 20 | line-height: 1; 21 | 22 | /* Better Font Rendering =========== */ 23 | -webkit-font-smoothing: antialiased; 24 | -moz-osx-font-smoothing: grayscale; 25 | } 26 | 27 | .muidocs-icon-communication-phone:before { 28 | content: "\e601"; 29 | } 30 | 31 | .muidocs-icon-communication-voicemail:before { 32 | content: "\e602"; 33 | } 34 | 35 | .muidocs-icon-navigation-expand-more:before { 36 | content: "\e603"; 37 | } 38 | 39 | .muidocs-icon-action-grade:before { 40 | content: "\e604"; 41 | } 42 | 43 | .muidocs-icon-action-home:before { 44 | content: "\e605"; 45 | } 46 | 47 | .muidocs-icon-action-stars:before { 48 | content: "\e606"; 49 | } 50 | 51 | .muidocs-icon-action-thumb-up:before { 52 | content: "\e607"; 53 | } 54 | 55 | .muidocs-icon-custom-sort:before { 56 | content: "\e600"; 57 | } 58 | 59 | .muidocs-icon-custom-github:before { 60 | content: "\e625"; 61 | } 62 | 63 | .muidocs-icon-custom-arrow-drop-right:before { 64 | content: "\e60b"; 65 | } 66 | 67 | .muidocs-icon-custom-pie:before { 68 | content: "\e610"; 69 | } 70 | 71 | -------------------------------------------------------------------------------- /src/public/css/fonts/roboto/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Capgemini/mesos-ui/b45798230528ac176820661696a770bf97903a7a/src/public/css/fonts/roboto/Roboto-Light.ttf -------------------------------------------------------------------------------- /src/public/css/fonts/roboto/Roboto-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Capgemini/mesos-ui/b45798230528ac176820661696a770bf97903a7a/src/public/css/fonts/roboto/Roboto-Medium.ttf -------------------------------------------------------------------------------- /src/public/css/fonts/roboto/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Capgemini/mesos-ui/b45798230528ac176820661696a770bf97903a7a/src/public/css/fonts/roboto/Roboto-Regular.ttf -------------------------------------------------------------------------------- /src/public/css/fonts/roboto/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Roboto'; 3 | font-style: normal; 4 | font-weight: 300; 5 | src: local('Roboto Light'), local('Roboto-Light'), url('Roboto-Light.ttf') format('truetype'); 6 | } 7 | @font-face { 8 | font-family: 'Roboto'; 9 | font-style: normal; 10 | font-weight: 400; 11 | src: local('Roboto'), local('Roboto-Regular'), url('Roboto-Regular.ttf') format('truetype'); 12 | } 13 | @font-face { 14 | font-family: 'Roboto'; 15 | font-style: normal; 16 | font-weight: 500; 17 | src: local('Roboto Medium'), local('Roboto-Medium'), url('Roboto-Medium.ttf') format('truetype'); 18 | } 19 | -------------------------------------------------------------------------------- /src/public/css/main.css: -------------------------------------------------------------------------------- 1 | /* flexboxgrid CSS */ 2 | @import "flexboxgrid.min.css"; 3 | 4 | /* Google Roboto font */ 5 | @import "fonts/roboto/style.css"; 6 | 7 | /* material design icons */ 8 | @import "font-icons/material-icons/style.css"; 9 | 10 | /* custom font icons */ 11 | @import "font-icons/material-ui-icons/style.css"; 12 | 13 | a { 14 | color: #ff4081; 15 | text-decoration: none; 16 | } 17 | 18 | a:hover { 19 | text-decoration: underline; 20 | } 21 | 22 | html { 23 | font-family: "Roboto","Helvetica Neue",Helvetica,Arial,sans-serif; 24 | -webkit-font-smoothing: antialiased; 25 | -moz-osx-font-smoothing: grayscale; 26 | } 27 | 28 | body, h1, h2, h3, h4, h5, h6 { 29 | margin: 0; 30 | } 31 | 32 | ul li { 33 | list-style: none; 34 | } 35 | 36 | ul { 37 | padding: 0; 38 | } 39 | 40 | body { 41 | font-size: 13px; 42 | line-height: 20px; 43 | } 44 | 45 | .box { 46 | padding-bottom: 5px; 47 | margin-bottom: 1rem; 48 | } 49 | 50 | .hide { 51 | display: none; 52 | } 53 | -------------------------------------------------------------------------------- /src/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Capgemini/mesos-ui/b45798230528ac176820661696a770bf97903a7a/src/public/favicon.ico -------------------------------------------------------------------------------- /src/public/humans.txt: -------------------------------------------------------------------------------- 1 | # humanstxt.org/ 2 | # The humans responsible & technology colophon 3 | 4 | # TEAM 5 | 6 | -- -- 7 | 8 | # THANKS 9 | 10 | 11 | 12 | # TECHNOLOGY COLOPHON 13 | 14 | CSS3, HTML5, JavaScript 15 | React, Flux, SuperAgent 16 | -------------------------------------------------------------------------------- /src/public/robots.txt: -------------------------------------------------------------------------------- 1 | # www.robotstxt.org/ 2 | 3 | # Allow crawling of all content 4 | User-agent: * 5 | Disallow: 6 | -------------------------------------------------------------------------------- /src/public/tile-wide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Capgemini/mesos-ui/b45798230528ac176820661696a770bf97903a7a/src/public/tile-wide.png -------------------------------------------------------------------------------- /src/public/tile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Capgemini/mesos-ui/b45798230528ac176820661696a770bf97903a7a/src/public/tile.png -------------------------------------------------------------------------------- /src/routes/api/mesos.js: -------------------------------------------------------------------------------- 1 | import request from 'superagent'; 2 | 3 | let config = require('../../config/config'); 4 | 5 | var mesos = { 6 | 7 | baseUrl: config.mesosEndpoint, 8 | 9 | /** 10 | * [getMetrics description] 11 | * @param {Function} callback [description] 12 | * @return {[type]} [description] 13 | */ 14 | getMetrics(callback) { 15 | let url = this.baseUrl + '/metrics/snapshot'; 16 | request 17 | .get(url) 18 | .end(callback); 19 | }, 20 | 21 | /** 22 | * [getState description] 23 | * @param {Function} callback [description] 24 | * @return {[type]} [description] 25 | */ 26 | getState(callback) { 27 | let url = this.baseUrl + '/master/state.json'; 28 | request 29 | .get(url) 30 | .end(callback); 31 | }, 32 | 33 | /** 34 | * [getSlaves description] 35 | * @param {Function} callback [description] 36 | * @return {[type]} [description] 37 | */ 38 | getSlaves(callback) { 39 | let url = this.baseUrl + '/master/slaves'; 40 | request 41 | .get(url) 42 | .end(callback); 43 | }, 44 | 45 | /** 46 | * [getLogs description] 47 | * @return {[type]} [description] 48 | */ 49 | getLogs(callback) { 50 | let url = this.baseUrl + '/files/read.json?path=/master/log&offset=-1'; 51 | request 52 | .get(url) 53 | .end(callback); 54 | }, 55 | 56 | }; 57 | 58 | module.exports = mesos 59 | 60 | 61 | -------------------------------------------------------------------------------- /src/routes/api/mesosFluxPropagator.js: -------------------------------------------------------------------------------- 1 | import request from 'superagent'; 2 | import ClusterStore from '../../stores/ClusterStore'; 3 | 4 | import mesos from './mesos' 5 | 6 | let mesosFluxPropagator = { 7 | 8 | propagateNewMetrics() { 9 | // Get metrics. 10 | mesos.getMetrics(function(err, response){ 11 | if (err) { 12 | console.log(err); 13 | return; 14 | } 15 | ClusterStore.metricsReceived(response.body); 16 | }); 17 | }, 18 | 19 | propagateNewLogs() { 20 | // Get metrics. 21 | mesos.getLogs(function(err, response){ 22 | let size = response.body.offset; 23 | let offset = parseInt(size-60000)>0 ? parseInt(size-60000) : 0; 24 | let url = mesos.baseUrl + '/files/read.json?path=/master/log&offset=' + offset + '&length=' + parseInt(offset+100000); 25 | request 26 | .get(url) 27 | .end(function(err, response){ 28 | if (err) { 29 | console.log(err); 30 | return; 31 | } 32 | ClusterStore.logsReceived(response.body); 33 | }); 34 | }); 35 | }, 36 | 37 | propagateNewState() { 38 | // Get state. 39 | mesos.getState(function(err, response){ 40 | if (err) { 41 | console.log(err); 42 | return; 43 | } 44 | ClusterStore.stateReceived(response.body); 45 | }); 46 | }, 47 | 48 | propagateMesosData() { 49 | let config = require('../../config/config'); 50 | this.propagateNewMetrics(); 51 | this.propagateNewState(); 52 | this.propagateNewLogs(); 53 | 54 | // Get metrics and state on interval 55 | setInterval(function(){ 56 | mesosFluxPropagator.propagateNewMetrics(); 57 | mesosFluxPropagator.propagateNewState(); 58 | mesosFluxPropagator.propagateNewLogs(); 59 | }, config.updateInterval); 60 | } 61 | }; 62 | 63 | 64 | module.exports = mesosFluxPropagator 65 | -------------------------------------------------------------------------------- /src/routes/dev.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import React from 'react'; 5 | import Router from 'react-router'; 6 | import routes from './react-routes'; 7 | import proxy from 'express-http-proxy'; 8 | import express from 'express'; 9 | 10 | module.exports = function(app) { 11 | 12 | var config = require('../config/config'); 13 | var router = express.Router(); 14 | 15 | // The top-level React component + HTML template for it 16 | let leader = process.env.MESOS_ENDPOINT; 17 | setRoutes(app, leader); 18 | 19 | function setRoutes(app, leader) { 20 | const templateFile = path.join(__dirname, 'master/static/index.html'); 21 | const template = _.template(fs.readFileSync(templateFile, 'utf8')); 22 | var config = require('../config/config'); 23 | 24 | app.get(config.proxyPath + '/*', proxy(leader, { 25 | forwardPath: function(req, res) { 26 | // Gets the path after 'proxy/'. 27 | let path = require('url').parse(req.url).path; 28 | return path.slice(config.mesosEndpoint.length); 29 | } 30 | })); 31 | 32 | app.get('*', function(req, res, next) { 33 | try { 34 | let data = { title: '', description: '', css: '', body: '' }; 35 | let css = []; 36 | Router.run(routes, req.url, function(Handler) { 37 | let application = ( css.push(value), 40 | onSetTitle: value => data.title = value, 41 | onSetMeta: (key, value) => data[key] = value 42 | }} /> 43 | ); 44 | data.body = React.renderToString(application); 45 | data.css = css.join(''); 46 | let html = template(data); 47 | res.send(html); 48 | }); 49 | } catch (err) { 50 | next(err); 51 | } 52 | }); 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /src/routes/react-routes.js: -------------------------------------------------------------------------------- 1 | /*jshint esnext: true */ 2 | import React from 'react'; 3 | import { Route, DefaultRoute, NotFoundRoute } from 'react-router'; 4 | 5 | import NotFound from '../pages/NotFound'; 6 | import Dashboard from '../pages/Dashboard'; 7 | import Nodes from '../pages/Nodes'; 8 | import Tasks from '../pages/Tasks'; 9 | import Frameworks from '../pages/Frameworks'; 10 | import Logs from '../pages/Logs'; 11 | import App from '../components/App'; 12 | 13 | 14 | export default ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | -------------------------------------------------------------------------------- /src/routes/zookeeper.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import React from 'react'; 5 | import Router from 'react-router'; 6 | import routes from './react-routes'; 7 | import proxy from 'express-http-proxy'; 8 | import express from 'express'; 9 | 10 | module.exports = function(app) { 11 | 12 | var zookeeper = require('node-zookeeper-client'); 13 | var config = require('../config/config'); 14 | var client = zookeeper.createClient(config.zookeeperAddress); 15 | var router = express.Router(); 16 | 17 | client.once('connected', function () { 18 | console.log('Connected to the Zookeeper.'); 19 | 20 | listChildren(client, config.zookeeperPath).then(function(childrenList) { 21 | // The smallest one is the leader. 22 | childrenList.sort(); 23 | return getData(client, config.zookeeperPath + '/' + childrenList[0]); 24 | }).then(function(jsonData) { 25 | // The top-level React component + HTML template for it 26 | let leader = 'http://' + jsonData.address.ip + ':' + jsonData.address.port; 27 | setRoutes(app, leader); 28 | }); 29 | }); 30 | client.connect(); 31 | 32 | function setRoutes(app, leader) { 33 | const templateFile = path.join(__dirname, 'master/static/index.html'); 34 | const template = _.template(fs.readFileSync(templateFile, 'utf8')); 35 | var config = require('../config/config'); 36 | 37 | app.get(config.proxyPath + '/*', proxy(leader, { 38 | forwardPath: function(req, res) { 39 | // Gets the path after 'proxy/'. 40 | let path = require('url').parse(req.url).path; 41 | return path.slice(config.mesosEndpoint.length); 42 | } 43 | })); 44 | 45 | app.get('*', function(req, res, next) { 46 | try { 47 | let data = { title: '', description: '', css: '', body: '' }; 48 | let css = []; 49 | Router.run(routes, req.url, function(Handler) { 50 | let application = ( css.push(value), 53 | onSetTitle: value => data.title = value, 54 | onSetMeta: (key, value) => data[key] = value 55 | }} /> 56 | ); 57 | data.body = React.renderToString(application); 58 | data.css = css.join(''); 59 | let html = template(data); 60 | res.send(html); 61 | }); 62 | } catch (err) { 63 | next(err); 64 | } 65 | }); 66 | } 67 | 68 | function listChildren(client, path) { 69 | return new Promise(function(resolve, reject) { 70 | client.getChildren( 71 | path, 72 | function (event) { 73 | console.log('Got watcher event: %s', event); 74 | listChildren(client, path).then(function(childrenList) { 75 | // The smallest one is the leader. 76 | childrenList.sort(); 77 | return getData(client, path + '/' + childrenList[0]); 78 | }).then(function(jsonData) { 79 | var config = require('../config/config'); 80 | // The top-level React component + HTML template for it 81 | let leader = 'http://' + jsonData.address.ip + ':' + jsonData.address.port; 82 | // Remove and recreate routes for new leader. 83 | app._router.stack.pop(); 84 | app._router.stack.pop(); 85 | setRoutes(app, leader); 86 | }); 87 | }, 88 | function (error, children, stat) { 89 | if (error) { 90 | console.log( 91 | 'Failed to list children of %s due to: %s.', 92 | path, 93 | error 94 | ); 95 | reject(Error('It broke')); 96 | } 97 | console.log('Children of %s are: %j.', path, children); 98 | resolve(children); 99 | } 100 | ); 101 | }); 102 | } 103 | 104 | function getData(client, path) { 105 | return new Promise(function(resolve, reject) { 106 | client.getData( 107 | path, 108 | function (event) { 109 | console.log('Got event: %s.', event); 110 | getData(client, path); 111 | }, 112 | function (error, data, stat) { 113 | if (error) { 114 | console.log(error.stack); 115 | reject(Error('It broke')); 116 | return; 117 | } 118 | console.log('Got data: %s', data.toString('utf8')); 119 | let string = data.toString('utf8'); 120 | let hash = JSON.parse(string); 121 | resolve(hash); 122 | } 123 | ); 124 | }); 125 | } 126 | }; 127 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | import 'babel/polyfill'; 2 | import path from 'path'; 3 | import express from 'express'; 4 | 5 | let config = require('./config/config'); 6 | let app = express(); 7 | 8 | // Serve static files from /public folder. 9 | app.set('port', config.port); 10 | app.use(express.static(path.join(__dirname))); 11 | app.listen(app.get('port'), () => { 12 | if (process.send) { 13 | process.send('online'); 14 | } else { 15 | console.log('The server is running at http://localhost:' + app.get('port')); 16 | } 17 | }); 18 | 19 | // Launch the server 20 | // ----------------------------------------------------------------------------- 21 | // // Default routes 22 | if (process.env.ZOOKEEPER_ADDRESS) { 23 | require('./routes/zookeeper')(app); 24 | } else if (process.env.MESOS_ENDPOINT) { 25 | require('./routes/dev')(app); 26 | } 27 | 28 | // If we're in development mode spin up a mock server as we may not have a 29 | // running Mesos cluster, so lets just use the stub API in ./stub.json. 30 | if (process.env.NODE_ENV === 'development') { 31 | let jsonServer = require('json-server'); 32 | let fs = require('fs'); 33 | 34 | // Returns an Express server 35 | let server = jsonServer.create(); 36 | 37 | // Set default middlewares (logger, static, cors and no-cache) 38 | server.use(jsonServer.defaults()); 39 | 40 | const stubFile = './src/stub.json'; 41 | let router = jsonServer.router(stubFile); 42 | server.use(router); 43 | 44 | server.listen(8000); 45 | } 46 | -------------------------------------------------------------------------------- /src/stores/ClusterStore.js: -------------------------------------------------------------------------------- 1 | import AppDispatcher from '../core/Dispatcher'; 2 | import EventEmitter from 'eventemitter3'; 3 | import ClusterConstants from '../constants/ClusterConstants'; 4 | import ClusterActions from '../actions/ClusterActions'; 5 | import assign from 'object-assign'; 6 | import _ from 'lodash'; 7 | 8 | var CHANGE_EVENT = 'change'; 9 | var METRICS_RECEIVED_EVENT = 'metricsReceived'; 10 | var LOGS_RECEIVED_EVENT = 'logsReceived'; 11 | var STATE_RECEIVED_EVENT = 'stateReceived'; 12 | 13 | var stats = require('./schemes/statsScheme.js'); 14 | 15 | var state = { 16 | nodes: [], 17 | frameworks: [], 18 | tasks: [], 19 | leader: '', 20 | pid: '' 21 | }; 22 | 23 | var logs = {}; 24 | 25 | /** 26 | * Refresh cluster stats 27 | */ 28 | function refreshStats(statistics) { 29 | assign(stats, 30 | { memFreeBytes: statistics['system/mem_free_bytes'] }, 31 | { memTotalBytes: statistics['system/mem_total_bytes'] }, 32 | { cpusPercent: statistics['master/cpus_percent'] }, 33 | { cpusUsed: statistics['master/cpus_used'] }, 34 | { cpusTotal: statistics['master/cpus_total'] }, 35 | { diskPercent: statistics['master/disk_percent'] }, 36 | { diskUsed: statistics['master/disk_used'] }, 37 | { diskTotal: statistics['master/disk_total'] }, 38 | { memPercent: statistics['master/mem_percent'] }, 39 | { memUsed: statistics['master/mem_used'] }, 40 | { memTotal: statistics['master/mem_total'] }, 41 | { slavesActive: statistics['master/slaves_active'] }, 42 | { slavesConnected: statistics['master/slaves_connected'] }, 43 | { slavesDisconnected: statistics['master/slaves_disconnected'] }, 44 | { slavesInactive: statistics['master/slaves_inactive'] }, 45 | { tasksRunning: statistics['master/tasks_running'] }, 46 | { tasksStaging: statistics['master/tasks_staging'] } 47 | ); 48 | } 49 | 50 | function refreshLogs(currentLogs) { 51 | Array.isArray(currentLogs) ? assign(logs, currentLogs[0]) : assign(logs, currentLogs); 52 | return logs; 53 | } 54 | 55 | function refreshState(data) { 56 | 57 | let frameworkScheme = require('./schemes/frameworkScheme.js'); 58 | let nodeScheme = require('./schemes/nodeScheme.js'); 59 | 60 | // Get the framework data. 61 | state.frameworks = data.frameworks.map(function(framework) { 62 | let frameworkData = assign({}, frameworkScheme); 63 | 64 | for(var propertyName in framework) { 65 | frameworkData[_.camelCase(propertyName)] = framework[propertyName]; 66 | } 67 | 68 | return frameworkData; 69 | }); 70 | 71 | // Get the node data. 72 | state.nodes = data.slaves.map(function(node) { 73 | let nodeData = assign({}, nodeScheme); 74 | 75 | for(var propertyName in node) { 76 | nodeData[_.camelCase(propertyName)] = node[propertyName]; 77 | } 78 | return nodeData; 79 | }); 80 | 81 | // This give us time complexity O(n) rather than O(n*m) 82 | // plus ease to change the patron for grouping at the component level. 83 | // O(n*m) shoudn't be a problem as number of frameworks never will be massive. 84 | let allTasks = []; 85 | let slaveIdName = {}; 86 | let frameworkIdName = {}; 87 | 88 | // Builds an array with all the tasks for all the frameworks. 89 | // Builds a hashs framewrokId: frameworkName. 90 | for(var index in data.frameworks) { 91 | frameworkIdName[data.frameworks[index].id] = data.frameworks[index].name; 92 | allTasks = allTasks.concat(data.frameworks[index].tasks); 93 | } 94 | 95 | // builds a hash slaveId: hostname. 96 | for(var index in data.slaves) { 97 | slaveIdName[data.slaves[index].id] = data.slaves[index].hostname; 98 | } 99 | 100 | // Add hostname, framework_name and timestamp at the task level. 101 | for(var index in allTasks) { 102 | allTasks[index].framework_name = frameworkIdName[allTasks[index].framework_id]; 103 | allTasks[index].hostname = slaveIdName[allTasks[index].slave_id]; 104 | allTasks[index].timestamp = allTasks[index].statuses.map((status) => { if (status.state == 'TASK_RUNNING') { return status.timestamp }} ); 105 | } 106 | 107 | state.leader = data.leader; 108 | state.pid = data.pid; 109 | state.tasks = allTasks; 110 | } 111 | 112 | var ClusterStore = assign({}, EventEmitter.prototype, { 113 | 114 | /** 115 | * Get the stats for the cluster. 116 | * @return {object} 117 | */ 118 | getStats() { 119 | return stats; 120 | }, 121 | 122 | getLogs() { 123 | return logs; 124 | }, 125 | 126 | getNodes() { 127 | return state.nodes; 128 | }, 129 | 130 | getFrameworks() { 131 | return state.frameworks; 132 | }, 133 | 134 | getTasks() { 135 | return state.tasks; 136 | }, 137 | 138 | getLeader() { 139 | return state.leader; 140 | }, 141 | 142 | getPid() { 143 | return state.pid; 144 | }, 145 | 146 | /** 147 | * Utility function to convert Megabytes to Gigabytes and round to the 148 | * closest 2 decimal places. 149 | * 150 | * [convertMBtoGB description] 151 | * @param {[type]} value [description] 152 | * @return {[type]} [description] 153 | */ 154 | convertMBtoGB(value) { 155 | return _.round(value / 1024, 2); 156 | }, 157 | 158 | /** 159 | * Emits change event to all registered event listeners. 160 | * 161 | * @returns {Boolean} Indication if we've emitted an event. 162 | */ 163 | emitChange() { 164 | return this.emit(CHANGE_EVENT); 165 | }, 166 | 167 | /** 168 | * @param {function} callback 169 | */ 170 | addChangeListener(callback) { 171 | this.on(CHANGE_EVENT, callback); 172 | }, 173 | 174 | /** 175 | * @param {function} callback 176 | */ 177 | removeChangeListener(callback) { 178 | this.removeListener(CHANGE_EVENT, callback); 179 | }, 180 | 181 | metricsReceived(data) { 182 | /* @todo - check if the data we are receiving is different from what we have 183 | already to avoid calling unecessary DOM updates */ 184 | ClusterActions.refreshStats(data); 185 | this.emit(METRICS_RECEIVED_EVENT, data); 186 | }, 187 | 188 | logsReceived(data) { 189 | /* @todo - check if the data we are receiving is different from what we have 190 | already to avoid calling unecessary DOM updates */ 191 | ClusterActions.refreshLogs(data); 192 | this.emit(LOGS_RECEIVED_EVENT, data); 193 | }, 194 | 195 | stateReceived(data) { 196 | /* @todo - check if the data we are receiving is different from what we have 197 | already to avoid calling unecessary DOM updates */ 198 | ClusterActions.refreshState(data); 199 | this.emit(STATE_RECEIVED_EVENT, data); 200 | } 201 | }); 202 | 203 | // Register callback to handle all updates 204 | ClusterStore.dispatchToken = AppDispatcher.register((action) => { 205 | 206 | switch(action.actionType) { 207 | 208 | case ClusterConstants.CLUSTER_REFRESH_STATS: 209 | refreshStats(action.stats); 210 | ClusterStore.emitChange(); 211 | break; 212 | 213 | case ClusterConstants.CLUSTER_REFRESH_LOGS: 214 | refreshLogs(action.logs); 215 | ClusterStore.emitChange(); 216 | break; 217 | 218 | case ClusterConstants.CLUSTER_REFRESH_STATE: 219 | refreshState(action.state); 220 | ClusterStore.emitChange(); 221 | break; 222 | 223 | default: 224 | // no op 225 | } 226 | }); 227 | 228 | export default ClusterStore; 229 | -------------------------------------------------------------------------------- /src/stores/__tests__/ClusterStore-test.js: -------------------------------------------------------------------------------- 1 | // __tests__/ClusterStore-test.js 2 | 3 | jest.dontMock('../../constants/ClusterConstants'); 4 | jest.dontMock('../ClusterStore'); 5 | jest.dontMock('object-assign'); 6 | jest.mock('../../core/Dispatcher'); 7 | 8 | describe('ClusterStore', function() { 9 | 10 | var ClusterConstants = require('../../constants/ClusterConstants'); 11 | var statsScheme = require('../schemes/statsScheme'); 12 | var Dispatcher, ClusterStore, callback; 13 | 14 | var mockStats = { 15 | "master/cpus_percent": 0.375, 16 | "master/cpus_total": 4, 17 | "master/cpus_used": 1.5, 18 | "master/disk_percent": 0, 19 | "master/disk_total": 7924, 20 | "master/disk_used": 0, 21 | "master/frameworks_active": 2, 22 | "master/frameworks_connected": 2, 23 | "master/frameworks_disconnected": 0, 24 | "master/frameworks_inactive": 0, 25 | "master/mem_percent": 0.186861313868613, 26 | "master/mem_total": 5480, 27 | "master/mem_used": 1024, 28 | "master/slaves_active": 2, 29 | "master/slaves_connected": 2, 30 | "master/slaves_disconnected": 0, 31 | "master/slaves_inactive": 0, 32 | "master/tasks_error": 0, 33 | "master/tasks_failed": 11, 34 | "master/tasks_finished": 0, 35 | "master/tasks_killed": 46, 36 | "master/tasks_lost": 0, 37 | "master/tasks_running": 1, 38 | "master/tasks_staging": 1, 39 | "master/tasks_starting": 0, 40 | "system/cpus_total": 1, 41 | "system/load_15min": 0.44, 42 | "system/load_1min": 0.15, 43 | "system/load_5min": 0.2, 44 | "system/mem_free_bytes": 2442649600, 45 | "system/mem_total_bytes": 3947372544 46 | } 47 | // mock actions 48 | var actionClusterRefreshStats = { 49 | actionType: ClusterConstants.CLUSTER_REFRESH_STATS, 50 | stats: mockStats 51 | }; 52 | 53 | beforeEach(function() { 54 | Dispatcher = require('../../core/Dispatcher'); 55 | ClusterStore = require('../ClusterStore'); 56 | callback = Dispatcher.register.mock.calls[0][0]; 57 | }); 58 | 59 | it('registers a callback with the dispatcher', function() { 60 | expect(Dispatcher.register.mock.calls.length).toBe(1); 61 | }); 62 | 63 | it('should initialize with no stats', function() { 64 | var all = ClusterStore.getStats(); 65 | expect(all).toEqual(statsScheme); 66 | }); 67 | 68 | it('refreshes cluster stats', function() { 69 | callback(actionClusterRefreshStats); 70 | var all = ClusterStore.getStats(); 71 | var keys = Object.keys(all); 72 | expect(keys.length).toBe(17); 73 | expect(all['memFreeBytes']).toEqual(2442649600); 74 | expect(all['cpusPercent']).toEqual(0.375); 75 | }); 76 | 77 | it('converts MB to GB correctly', function() { 78 | // 7924 MB = 7.74 GB 79 | expect(ClusterStore.convertMBtoGB(7924)).toEqual(7.74); 80 | 81 | // Zero values 82 | expect(ClusterStore.convertMBtoGB(0)).toEqual(0); 83 | 84 | // 100 MB 85 | expect(ClusterStore.convertMBtoGB(0.1)).toEqual(0); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /src/stores/schemes/frameworkScheme.js: -------------------------------------------------------------------------------- 1 | var frameworkScheme = { 2 | 'active': true, 3 | 'checkpoint': true, 4 | 'completedTasks': [], 5 | 'executors': [], 6 | 'failoverTimeout': null, 7 | 'hostname': null, 8 | 'id': null, 9 | 'name': null, 10 | 'offeredResources': {}, 11 | 'offers': [], 12 | 'pid': null, 13 | 'registeredTime': null, 14 | 'resources': {}, 15 | 'role': '*', 16 | 'tasks': [], 17 | 'unregisteredTime': 0, 18 | 'usedResources': {}, 19 | 'user': 'root', 20 | 'webuiUrl': null 21 | }; 22 | 23 | module.exports = frameworkScheme; 24 | -------------------------------------------------------------------------------- /src/stores/schemes/nodeScheme.js: -------------------------------------------------------------------------------- 1 | var nodeScheme = { 2 | 'active': true, 3 | 'attributes': {}, 4 | 'hostname': null, 5 | 'id': null, 6 | 'offeredResources': {}, 7 | 'pid': null, 8 | 'registeredTime': null, 9 | 'reservedResources': {}, 10 | 'resources': {}, 11 | 'unreservedResources': {}, 12 | 'usedResources': {} 13 | }; 14 | 15 | module.exports = nodeScheme; 16 | -------------------------------------------------------------------------------- /src/stores/schemes/statsScheme.js: -------------------------------------------------------------------------------- 1 | var statsScheme = { 2 | memFreeBytes: 0, 3 | memTotalBytes: 0, 4 | cpusPercent: 0, 5 | cpusUsed: 0, 6 | cpusTotal: 0, 7 | diskPercent: 0, 8 | diskUsed: 0, 9 | diskTotal: 0, 10 | memPercent: 0, 11 | memUsed: 0, 12 | memTotal: 0, 13 | slavesActive: 0, 14 | slavesConnected: 0, 15 | slavesDisconnected: 0, 16 | slavesInactive: 0, 17 | tasksRunning: 0, 18 | tasksStaging: 0 19 | }; 20 | 21 | module.exports = statsScheme; 22 | -------------------------------------------------------------------------------- /src/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <%- title %> 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 |
    <%= body %>
    19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | import webpack, { DefinePlugin, BannerPlugin } from 'webpack'; 2 | import merge from 'lodash/object/merge'; 3 | import autoprefixer from 'autoprefixer-core'; 4 | import minimist from 'minimist'; 5 | 6 | const argv = minimist(process.argv.slice(2)); 7 | const DEBUG = !argv.release; 8 | const STYLE_LOADER = 'style-loader/useable'; 9 | const CSS_LOADER = DEBUG ? 'css-loader' : 'css-loader?minimize'; 10 | const AUTOPREFIXER_BROWSERS = [ 11 | 'Android 2.3', 12 | 'Android >= 4', 13 | 'Chrome >= 20', 14 | 'Firefox >= 24', 15 | 'Explorer >= 8', 16 | 'iOS >= 6', 17 | 'Opera >= 12', 18 | 'Safari >= 6' 19 | ]; 20 | const GLOBALS = { 21 | 'process.env.NODE_ENV': DEBUG ? '"development"' : '"production"', 22 | '__DEV__': DEBUG 23 | }; 24 | 25 | // 26 | // Common configuration chunk to be used for both 27 | // client-side (app.js) and server-side (server.js) bundles 28 | // ----------------------------------------------------------------------------- 29 | 30 | const config = { 31 | output: { 32 | publicPath: './', 33 | sourcePrefix: ' ' 34 | }, 35 | 36 | cache: DEBUG, 37 | debug: DEBUG, 38 | 39 | stats: { 40 | colors: true, 41 | reasons: DEBUG 42 | }, 43 | 44 | plugins: [ 45 | new webpack.optimize.OccurenceOrderPlugin() 46 | ], 47 | 48 | resolve: { 49 | extensions: ['', '.webpack.js', '.web.js', '.js', '.jsx'] 50 | }, 51 | 52 | module: { 53 | preLoaders: [ 54 | { 55 | test: /\.js$/, 56 | exclude: /node_modules/, 57 | loader: 'eslint-loader' 58 | } 59 | ], 60 | 61 | loaders: [ 62 | { 63 | test: /\.css$/, 64 | loader: `${STYLE_LOADER}!${CSS_LOADER}!postcss-loader` 65 | }, 66 | { 67 | test: /\.scss$/, 68 | loader: `${STYLE_LOADER}!${CSS_LOADER}!postcss-loader!sass-loader` 69 | }, 70 | { 71 | test: /\.gif/, 72 | loader: 'url-loader?limit=10000&mimetype=image/gif' 73 | }, 74 | { 75 | test: /\.jpg/, 76 | loader: 'url-loader?limit=10000&mimetype=image/jpg' 77 | }, 78 | { 79 | test: /\.png/, 80 | loader: 'url-loader?limit=10000&mimetype=image/png' 81 | }, 82 | { 83 | test: /\.svg/, 84 | loader: 'url-loader?limit=10000&mimetype=image/svg+xml' 85 | }, 86 | { 87 | test: /\.jsx?$/, 88 | loader: 'transform?envify', 89 | }, 90 | { 91 | test: /\.json?$/, 92 | loader: 'file-loader' 93 | }, 94 | { 95 | test: /\.jsx?$/, 96 | exclude: /node_modules/, 97 | loader: 'babel-loader' 98 | } 99 | ] 100 | }, 101 | 102 | postcss: [autoprefixer(AUTOPREFIXER_BROWSERS)] 103 | }; 104 | 105 | // 106 | // Configuration for the client-side bundle (app.js) 107 | // ----------------------------------------------------------------------------- 108 | 109 | const appConfig = merge({}, config, { 110 | entry: './src/app.js', 111 | output: { 112 | path: './build/master/static', 113 | filename: 'app.js' 114 | }, 115 | node: { 116 | fs: 'empty', 117 | }, 118 | devtool: DEBUG ? 'source-map' : false, 119 | plugins: config.plugins.concat([ 120 | new DefinePlugin(merge(GLOBALS, {'__SERVER__': false})) 121 | ].concat(DEBUG ? [] : [ 122 | new webpack.optimize.DedupePlugin(), 123 | new webpack.optimize.UglifyJsPlugin(), 124 | new webpack.optimize.AggressiveMergingPlugin() 125 | ]) 126 | ) 127 | }); 128 | 129 | // 130 | // Configuration for the server-side bundle (server.js) 131 | // ----------------------------------------------------------------------------- 132 | 133 | const serverConfig = merge({}, config, { 134 | entry: './src/server.js', 135 | output: { 136 | path: './build', 137 | filename: 'server.js', 138 | libraryTarget: 'commonjs2' 139 | }, 140 | target: 'node', 141 | externals: /^[a-z][a-z\.\-0-9]*$/, 142 | node: { 143 | console: false, 144 | global: false, 145 | process: false, 146 | Buffer: false, 147 | __filename: false, 148 | __dirname: false 149 | }, 150 | devtool: DEBUG ? 'source-map' : 'cheap-module-source-map', 151 | plugins: config.plugins.concat( 152 | new DefinePlugin(merge(GLOBALS, {'__SERVER__': true})), 153 | new BannerPlugin('require("source-map-support").install();', 154 | { raw: true, entryOnly: false }) 155 | ), 156 | module: { 157 | loaders: config.module.loaders.map(function(loader) { 158 | // Remove style-loader 159 | return merge(loader, { 160 | loader: loader.loader = loader.loader.replace(STYLE_LOADER + '!', '') 161 | }); 162 | }) 163 | } 164 | }); 165 | 166 | export default [appConfig, serverConfig]; 167 | -------------------------------------------------------------------------------- /wercker.yml: -------------------------------------------------------------------------------- 1 | box: node:4.3.2 2 | build: 3 | steps: 4 | - npm-install 5 | - npm-test 6 | - script: 7 | name: collect coverage 8 | code: | 9 | npm install -g codeclimate-test-reporter 10 | CODECLIMATE_REPO_TOKEN=$CODECLIMATE_REPO_TOKEN codeclimate-test-reporter < coverage/lcov.info 11 | - script: 12 | name: echo nodejs information 13 | code: | 14 | echo "node version $(node -v) running" 15 | echo "npm version $(npm -v) running" 16 | --------------------------------------------------------------------------------