├── .babelrc
├── .editorconfig
├── .eslintrc
├── .gitignore
├── .jscsrc
├── Gruntfile.js
├── LICENSE
├── README.md
├── circle.yml
├── docs
└── example.jpg
├── package.json
├── src
├── app
│ ├── ConfigurationStore.js
│ ├── PasswordPrompt.js
│ ├── StatusDashboard.js
│ ├── StatusIndicator.js
│ └── StatusStore.js
├── config.json
├── favicon.png
├── flux
│ ├── Action.js
│ ├── CachingStore.js
│ ├── Store.js
│ └── SubscribeMixin.js
├── index.html
├── main.js
├── source
│ ├── DockerCloudService.js
│ ├── DropwizardHealthcheck.js
│ ├── GithubBranches.js
│ ├── Loggly.js
│ ├── Message.js
│ ├── RssAws.js
│ ├── RssBase.js
│ ├── Source.js
│ ├── SourceTypes.js
│ ├── StatusCode.js
│ ├── StatusIo.js
│ ├── VstsBase.js
│ ├── VstsBranches.js
│ └── VstsBuild.js
├── style.less
├── touch-icon.png
└── util
│ ├── AppVersion.js
│ ├── BuildUtils.js
│ ├── Logger.js
│ ├── Mixins.js
│ ├── RegExps.js
│ └── UrlParameters.js
└── test
├── TestUtils.js
├── es6Setup.js
├── flux
├── ActionTest.js
├── CachingStoreTest.js
├── StoreTest.js
└── SubscribeMixinTest.js
├── source
└── DropwizardHealthcheckTest.js
├── testSetup.js
└── util
├── AppVersionTest.js
├── BuildUtilsTest.js
├── LoggerTest.js
├── MixinsTest.js
└── RegExpsTest.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": ["transform-runtime", "transform-object-rest-spread"],
3 | "presets": ["es2015", "react"],
4 | }
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*]
2 | indent_style = space
3 | indent_size = 4
4 | charset = utf-8
5 | end_of_line = crlf
6 |
7 | [*.yml]
8 | indent_size = 2
9 |
10 | [package.json]
11 | indent_size = 2
12 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parserOptions": {
3 | "ecmaVersion": 6,
4 | "ecmaFeatures": {
5 | "experimentalObjectRestSpread": true,
6 | "jsx": true
7 | },
8 | "sourceType": "module"
9 | },
10 | "env": {
11 | "browser": true,
12 | "mocha": true,
13 | "es6": true
14 | },
15 | "globals": {
16 | "__filename": true,
17 | "process": true
18 | },
19 | "plugins": [
20 | "react",
21 | "lodash"
22 | ],
23 | "extends": "eslint:recommended",
24 | "rules": {
25 | "consistent-return": 2,
26 | "curly": 2,
27 | "dot-location": [2, "property"],
28 | "dot-notation": 2,
29 | "eqeqeq": 2,
30 | "no-alert": 2,
31 | "no-caller": 2,
32 | "no-case-declarations": 2,
33 | "no-eval": 2,
34 | "no-extend-native": 2,
35 | "no-implied-eval": 2,
36 | "no-invalid-this": 2,
37 | "no-lone-blocks": 2,
38 | "no-multi-spaces": 2,
39 | "no-native-reassign": 2,
40 | "no-new-func": 2,
41 | "no-new-wrappers": 2,
42 | "no-return-assign": 2,
43 | "no-script-url": 2,
44 | "no-self-compare": 2,
45 | "no-sequences": 2,
46 | "no-throw-literal": 2,
47 | "no-useless-call": 2,
48 | "no-void": 2,
49 | "no-with": 2,
50 | "radix": 2,
51 |
52 | "no-catch-shadow": 2,
53 | "no-shadow": 2,
54 |
55 | "array-bracket-spacing": 2,
56 | "brace-style": 2,
57 | "comma-spacing": 2,
58 | "comma-style": 2,
59 | "computed-property-spacing": 2,
60 | "indent": 2,
61 | "jsx-quotes": 2,
62 | "key-spacing": 2,
63 | "keyword-spacing": 2,
64 | "new-cap": 2,
65 | "new-parens": 2,
66 | "no-array-constructor": 2,
67 | "no-new-object": 2,
68 | "no-spaced-func": 2,
69 | "no-trailing-spaces": 2,
70 | "no-unneeded-ternary": 2,
71 | "object-curly-spacing": 2,
72 | "quotes": 2,
73 | "semi-spacing": 2,
74 | "semi": 2,
75 | "space-before-blocks": 2,
76 | "space-in-parens": 2,
77 | "space-infix-ops": 2,
78 | "space-unary-ops": 2,
79 |
80 | "arrow-parens": [2, "as-needed"],
81 | "arrow-spacing": 2,
82 | "constructor-super": 2,
83 | "no-class-assign": 2,
84 | "no-const-assign": 2,
85 | "no-dupe-class-members": 2,
86 | "no-this-before-super": 2,
87 | "object-shorthand": [2, "methods"],
88 | "prefer-arrow-callback": 2,
89 | "prefer-spread": 2,
90 |
91 | "no-implicit-globals": 2,
92 | "no-new-symbol": 2,
93 | "no-useless-constructor": 2,
94 | "no-whitespace-before-property": 2,
95 | "prefer-rest-params": 2,
96 | "template-curly-spacing": 2,
97 |
98 | "react/jsx-curly-spacing": 2,
99 | "react/jsx-no-duplicate-props": 2,
100 | "react/jsx-no-undef": 2,
101 | "react/jsx-uses-react": 2,
102 | "react/jsx-uses-vars": 2,
103 | "react/no-did-mount-set-state": 2,
104 | "react/no-did-update-set-state": 2,
105 | "react/no-direct-mutation-state": 2,
106 | "react/no-unknown-property": 2,
107 | "react/prefer-es6-class": 2,
108 | "react/react-in-jsx-scope": 2,
109 | "react/self-closing-comp": 2,
110 | "react/sort-comp": 2,
111 | "react/wrap-multilines": 2,
112 |
113 | "lodash/no-double-unwrap": 2,
114 | "lodash/chain-style": [2, "explicit"],
115 | "lodash/prefer-lodash-method": 2,
116 | "lodash/prefer-lodash-typecheck": 2,
117 | "lodash/prefer-startswith": 2
118 | }
119 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea
2 | *.iml
3 | target
4 | node_modules
5 | npm-debug.log
--------------------------------------------------------------------------------
/.jscsrc:
--------------------------------------------------------------------------------
1 | {
2 | "disallowKeywords": ["with"],
3 | "disallowMixedSpacesAndTabs": true,
4 | "disallowNewlineBeforeBlockStatements": true,
5 | "disallowQuotedKeysInObjects": "allButReserved",
6 | "disallowSpaceAfterObjectKeys": true,
7 | "disallowSpaceAfterPrefixUnaryOperators": true,
8 | "disallowSpaceBeforePostfixUnaryOperators": true,
9 | "disallowSpacesInCallExpression": true,
10 | "disallowSpacesInNamedFunctionExpression": {"beforeOpeningRoundBrace": true},
11 | "disallowSpacesInsideParentheses": true,
12 | "disallowTrailingComma": true,
13 | "disallowTrailingWhitespace": "ignoreEmptyLines",
14 |
15 | "requireBlocksOnNewline": true,
16 | "requireCamelCaseOrUpperCaseIdentifiers": "ignoreProperties",
17 | "requireCapitalizedConstructors": true,
18 | "requireCapitalizedConstructorsNew": true,
19 | "requireCommaBeforeLineBreak": true,
20 | "requireCurlyBraces": true,
21 | "requireDotNotation": true,
22 | "requireParenthesesAroundIIFE": true,
23 | "requireSpaceAfterBinaryOperators": true,
24 | "requireSpaceAfterKeywords": true,
25 | "requireSpaceBeforeBlockStatements": true,
26 | "requireSpaceBeforeObjectValues": true,
27 | "requireSpaceBetweenArguments": true,
28 | "requireSpacesInForStatement": true,
29 |
30 | "validateIndentation": 4,
31 | "validateParameterSeparator": ", ",
32 | "validateQuoteMarks": "\"",
33 |
34 | "esnext": true
35 | }
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | /*global module, require, process*/
2 | /*eslint prefer-arrow-callback: 0, no-invalid-this: 0, object-curly-spacing: 0*/
3 | var webpack = require("webpack");
4 | var _ = require("lodash");
5 | var HtmlPlugin = require("html-webpack-plugin");
6 | var ExtractTextPlugin = require("extract-text-webpack-plugin");
7 | var CompressionPlugin = require("compression-webpack-plugin");
8 |
9 | module.exports = function (grunt) {
10 | grunt.initConfig({});
11 |
12 | var pkg = grunt.file.readJSON("./package.json");
13 |
14 | grunt.loadNpmTasks("grunt-webpack");
15 | grunt.config.set("webpack", {
16 | build: {
17 | context: "src",
18 | entry: {
19 | main: "./main.js",
20 | vendor: _.without(_.keys(pkg.dependencies), "bootstrap", "bootswatch")
21 | },
22 | output: {
23 | path: "target/dist",
24 | filename: "main-[chunkhash].min.js"
25 | },
26 | module: {
27 | loaders: [{
28 | loader: "babel",
29 | test: /\.js$/,
30 | exclude: /node_modules/,
31 | query: {
32 | cacheDirectory: true
33 | }
34 | }, {
35 | loader: ExtractTextPlugin.extract("style", "css"),
36 | test: /\.css$/
37 | }, {
38 | loader: ExtractTextPlugin.extract("style", "css!less"),
39 | test: /\.less$/
40 | }, {
41 | loader: "file",
42 | test: /\.(png|jpg|woff2?|ttf|eot|svg)$/,
43 | query: {
44 | name: "[name]-[hash].[ext]"
45 | }
46 | }, {
47 | loader: "json",
48 | test: /\.json$/
49 | }]
50 | },
51 | plugins: [
52 | // Keep the same module order between builds so the output file stays the same if there are no changes.
53 | new webpack.optimize.OccurenceOrderPlugin(),
54 | new webpack.optimize.CommonsChunkPlugin("vendor", "vendor-[chunkhash].min.js"),
55 | new HtmlPlugin({
56 | template: "./index.html"
57 | }),
58 | new webpack.DefinePlugin({
59 | "process.env": {
60 | // Disable React's development checks.
61 | NODE_ENV: JSON.stringify("production")
62 | }
63 | }),
64 | new ExtractTextPlugin("main-[contenthash].css"),
65 | new webpack.optimize.UglifyJsPlugin({
66 | minimize: true,
67 | // Remove all comments.
68 | comments: /a^/g,
69 | compress: {
70 | warnings: false
71 | }
72 | }),
73 | new CompressionPlugin(),
74 | new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
75 | ],
76 | node: {
77 | __filename: true
78 | },
79 | progress: false
80 | }
81 | });
82 |
83 | grunt.config.set("webpack-dev-server", {
84 | start: {
85 | webpack: {
86 | context: "src",
87 | entry: [
88 | "webpack-dev-server/client?http://localhost:8080",
89 | "webpack/hot/only-dev-server",
90 | "./main.js"
91 | ],
92 | output: {
93 | path: "target",
94 | filename: "main.js"
95 | },
96 | module: {
97 | loaders: [{
98 | loader: "babel",
99 | test: /\.js$/,
100 | exclude: /node_modules/,
101 | query: {
102 | cacheDirectory: true,
103 | plugins: [["react-transform", {
104 | transforms: [{
105 | transform: "react-transform-hmr",
106 | imports: ["react"],
107 | locals: ["module"]
108 | }, {
109 | transform: "react-transform-catch-errors",
110 | imports: ["react", "redbox-react"]
111 | }]
112 | }]]
113 | }
114 | }, {
115 | loader: "style!css",
116 | test: /\.css$/
117 | }, {
118 | loader: "style!css!less",
119 | test: /\.less$/
120 | }, {
121 | loader: "file",
122 | test: /\.(png|jpg|woff2?|ttf|eot|svg)$/,
123 | query: {
124 | name: "name=[name]-[hash].[ext]"
125 | }
126 | }, {
127 | loader: "json",
128 | test: /\.json$/
129 | }]
130 | },
131 | plugins: [
132 | new HtmlPlugin({
133 | template: "./index.html"
134 | }),
135 | new webpack.HotModuleReplacementPlugin(),
136 | new webpack.DefinePlugin({
137 | "process.env": {
138 | NODE_ENV: JSON.stringify("development")
139 | }
140 | })
141 | ],
142 | node: {
143 | __filename: true
144 | },
145 | watch: true,
146 | keepalive: true,
147 | devtool: "cheap-module-eval-source-map"
148 | },
149 | publicPath: "/",
150 | hot: true,
151 | keepAlive: true
152 | }
153 | });
154 |
155 | grunt.loadNpmTasks("grunt-jscs");
156 | var src = ["src/**/*.js", "test/**/*.js", "Gruntfile.js"];
157 | grunt.config.set("jscs", {
158 | options: {
159 | config: ".jscsrc"
160 | },
161 | dev: {
162 | src: src
163 | },
164 | fix: {
165 | options: {
166 | fix: true
167 | },
168 | src: src
169 | },
170 | ci: {
171 | options: {
172 | reporter: "junit",
173 | reporterOutput: "target/style.xml"
174 | },
175 | src: src
176 | }
177 | });
178 |
179 | grunt.loadNpmTasks("grunt-eslint");
180 | grunt.config.set("eslint", {
181 | options: {
182 | configFile: ".eslintrc"
183 | },
184 | dev: {
185 | src: src
186 | },
187 | ci: {
188 | options: {
189 | format: "junit",
190 | outputFile: "target/lint.xml"
191 | },
192 | src: src
193 | }
194 | });
195 |
196 | grunt.loadNpmTasks("grunt-mocha-test");
197 | var testSrc = ["test/**/*Test.js"];
198 | grunt.config.set("mochaTest", {
199 | options: {
200 | require: [
201 | "test/es6Setup",
202 | "test/testSetup"
203 | ]
204 | },
205 | dev: {
206 | src: testSrc
207 | },
208 | ci: {
209 | options: {
210 | reporter: "xunit",
211 | captureFile: "target/tests.xml",
212 | quiet: true
213 | },
214 | src: testSrc
215 | }
216 | });
217 |
218 | grunt.loadNpmTasks("grunt-contrib-clean");
219 | grunt.config.set("clean", {
220 | dist: ["target/dist/*"]
221 | });
222 |
223 | grunt.loadNpmTasks("grunt-aws");
224 | grunt.config.set("s3", {
225 | options: {
226 | accessKeyId: "",
227 | secretAccessKey: "",
228 | region: "us-east-1",
229 | bucket: ""
230 | },
231 | upload: {
232 | files: [{
233 | cwd: "target/dist/",
234 | src: "**",
235 | dest: "status/"
236 | }, {
237 | cwd: "src/",
238 | src: "config.json",
239 | dest: "status/"
240 | }]
241 | }
242 | });
243 |
244 |
245 | grunt.registerTask("dev", ["webpack-dev-server:start"]);
246 | grunt.registerTask("test", ["eslint:dev", "jscs:dev", "mochaTest:dev"]);
247 | grunt.registerTask("build", ["clean:dist", "webpack:build"]);
248 | grunt.registerTask("upload", ["s3:upload"]);
249 |
250 | grunt.registerTask("ci", ["eslint:ci", "jscs:ci", "mochaTest:ci", "build"]);
251 | };
252 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Bo Gotthardt
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND 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 THE
21 | SOFTWARE.
22 |
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # simple-dashboard
2 | [](https://circleci.com/gh/Lugribossk/simple-dashboard)
3 | [](https://david-dm.org/Lugribossk/simple-dashboard)
4 | [](https://david-dm.org/Lugribossk/simple-dashboard#info=devDependencies)
5 |
6 | A straightforward dashboard for showing an overview of the status of servers and infrastructure.
7 | Runs entirely in the browser as static Javascript so it can be hosted easily without needing to set up and maintain yet another server.
8 |
9 | 
10 |
11 | ## Configuration
12 | The dashboard is configured via a JSON config file that defines where to get status information from.
13 |
14 | ```json
15 | {
16 | "title": "Infrastructure status",
17 | "sources": [{
18 | "type": "statusio",
19 | "title": "Docker",
20 | "link": "http://status.docker.com",
21 | "id": "533c6539221ae15e3f000031"
22 | }, {
23 | "type": "rss-aws",
24 | "title": "CloudFront",
25 | "id": "cloudfront"
26 | }, {
27 | "type": "rss-aws",
28 | "title": "EC2 US East",
29 | "id": "ec2-us-east-1"
30 | }, {
31 | "type": "dropwizard",
32 | "title": "Production - Healthcheck",
33 | "adminPath": "http://localhost:9090/admin"
34 | }]
35 | }
36 | ```
37 |
38 | Name|Description
39 | ---|---
40 | title|Title to show at the top of the dashboard. Optional.
41 | sources|List of status sources to show and their individual configurations.
42 | panels|List of panels that split the screen
43 |
44 | ## Status sources
45 |
46 | ### General options
47 | Options for all the status sources.
48 |
49 | Name|Default|Description
50 | ---|---|---
51 | type||Which kind of source this is, must be one of the types listed below, e.g. `status-code` or `vsts-branches`.
52 | title||Title displayed on status indicator, e.g. `Production Healthcheck`.
53 | interval|60|Number of seconds between status checks.
54 |
55 | ### Docker Cloud service
56 | Status of a [Docker Cloud](https://cloud.docker.com/) (formerly Tutum) service.
57 |
58 | Name|Default|Description
59 | ---|---|---
60 | type||`docker-cloud-service`
61 | id||Service ID.
62 | username||Docker Cloud account username.
63 | apiKey||Docker Cloud account API key.
64 |
65 | ### Dropwizard healthcheck
66 | The status of a [Dropwizard](http://www.dropwizard.io) service's [health checks](http://www.dropwizard.io/manual/core.html#health-checks).
67 |
68 | By default Dropwizard is not set up to allow cross-origin requests, so you will have to add a servlet filter to the admin port that does this.
69 | TODO example.
70 |
71 | Name|Default|Description
72 | ---|---|---
73 | type||`dropwizard`
74 | adminPath||Path to the admin port for your service, e.g. `http://localhost:8081` for a local server with the default admin settings.
75 |
76 | ### GitHub branches
77 | All the branches of a GitHub repository. Also shows any open pull requests from those branches to master.
78 |
79 | Can also show the [status](https://developer.github.com/v3/repos/statuses/) of the latest commit in each branch.
80 | This is set by many build system that integrate with GitHub such as CircleCI.
81 |
82 | Name|Default|Description
83 | ---|---|---
84 | type||`github-branches`
85 | owner||Repository owner name, i.e. the user or organization the repo is located under.
86 | repo||Repository name.
87 | token||Personal access token.
88 | showStatus|false|Also show build status. The build status is only set if an external system pushes it to Github, e.g. as part of a continuous integration setup with Travis or CircleCI.
89 |
90 | ### Loggly
91 | Number of WARN and ERROR log messages in [Loggly](http://www.loggly.com).
92 |
93 | Name|Default|Description
94 | ---|---|---
95 | type||`loggly`
96 | username||Username.
97 | password||Password.
98 | account||Account name, from the Loggly URL.
99 | tag||A tag to filter by, e.g. to separate logs from different environments.
100 | from|`-24h`|Count log messages newer than this.
101 |
102 |
103 | ### Static message
104 | A static message.
105 |
106 | Name|Default|Description
107 | ---|---|---
108 | type||`message`
109 | status|success|How the status indicator should look, either `success`, `warning`, `danger` or `info`.
110 | message||Message to display.
111 |
112 | ### Amazon Web Services status
113 | One of the statuses from Amazon Web Services' [Service Health Dashboard](http://status.aws.amazon.com/).
114 |
115 | Name|Default|Description
116 | ---|---|---
117 | type||`rss-aws`
118 | id||ID of the status feed to follow as seen in the RSS link, e.g. `ec2-us-east-1`.
119 |
120 | ### Response status code
121 | Whether an arbitrary URL returned a successful status code. Any status code below 400 counts as successful.
122 |
123 | Make sure that the server is set up to allow cross-origin requests.
124 |
125 | Name|Default|Description
126 | ---|---|---
127 | type||`status-code`
128 | url||URL to request and check response status code for.
129 | link|url|Link when clicking on the status indicator.
130 |
131 | ### Status.io
132 | Status from a service dashboard hosted by [Status.io](http://status.io). Many web services use this for their status pages.
133 |
134 | Name|Default|Description
135 | ---|---|---
136 | type||`statusio`
137 | id||Status.io's ID for the service you want to check, e.g. `533c6539221ae15e3f000031` for Docker. There doesn't seem to be an easy way to find this yourself, but you can probably get it by asking customer support for the service you want to check.
138 | link||Link to the service's status page, e.g. `https://status.docker.com`.
139 |
140 | ### Visual Studio Team Services branches
141 | Build status of the latest commit for all the branches in a Visual Studio Team Services Git repository. Also shows highlights branches with an open pull request to master.
142 |
143 | Name|Default|Description
144 | ---|---|---
145 | type||`vsts-branches`
146 | repoId|ID of the repository, can be found in the URL in the control panel under Version Control.
147 | account|Account subdomain.
148 | project|Project name.
149 | token|[Personal Access Token](https://www.visualstudio.com/en-us/get-started/setup/use-personal-access-tokens-to-authenticate).
150 |
151 | ### Visual Studio Team Services build
152 | Build status of the latest commit for a single branch in a Visual Studio Team Services Git repository.
153 |
154 | Name|Default|Description
155 | ---|---|---
156 | type||`vsts-build`
157 | branch|master|Branch to show status for.
158 | definition||Name of the build definition to show status for.
159 | account|
160 | project|
161 | token|
162 |
163 |
164 |
165 | ## Complications
166 |
167 | ### Credentials
168 |
169 | If you put secrets such as Github tokens in the configuration file, then you should either encrupt the secret or only upload the dashboard to a non-public site.
170 |
171 | Values can be encrypted with `window.encrypt("password", "value")` which should then be placed in config.json as e.g. `{"token": {"encrypted": "..."}}`
172 |
173 | ### Cross-origin requests
174 |
175 | TODO
176 |
177 |
178 | ## Setup
179 | - Install NodeJS
180 | - `npm install -g grunt-cli`
181 | - `npm install`
182 |
183 | ## Development
184 | - `grunt dev`
185 | - Open `localhost:8080`
186 | - The development configuration file will be loaded directly from `src/config.json`
187 |
188 | ### Adding new source types
189 |
190 | 1. Create a new subclass of `Source` that overrides `getStatus()`.
191 | 2. Define its type in the configuration file by adding it as a static property on your subclass named `type`.
192 | 3. Add it to the list in `SourceTypes`.
193 |
194 | ## Building
195 | - `grunt build`
196 | - The files in `target/dist` can then be placed on a server. The real config.json configuration file you want to use should be placed next to index.html.
--------------------------------------------------------------------------------
/circle.yml:
--------------------------------------------------------------------------------
1 | machine:
2 | node:
3 | version: 4.2.2
4 |
5 | dependencies:
6 | override:
7 | - npm install -g grunt-cli
8 | - npm install
9 |
10 | test:
11 | override:
12 | - grunt ci
13 | - cp target/*.xml $CIRCLE_TEST_REPORTS
14 |
--------------------------------------------------------------------------------
/docs/example.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Lugribossk/simple-dashboard/a8058d98e6add724df8e758cd5135c21a4f4fea4/docs/example.jpg
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "simple-dashboard",
3 | "version": "1.0.0",
4 | "private": true,
5 | "dependencies": {
6 | "bluebird": "3.3.5",
7 | "bootstrap": "3.3.6",
8 | "bootswatch": "3.3.6",
9 | "immutable": "3.8.1",
10 | "lodash": "4.4.0",
11 | "moment": "2.13.0",
12 | "piecon": "0.5.0",
13 | "react": "15.0.2",
14 | "react-addons-linked-state-mixin": "15.0.2",
15 | "react-addons-pure-render-mixin": "15.0.2",
16 | "react-bootstrap": "0.30.7",
17 | "react-dom": "15.0.2",
18 | "sjcl": "1.0.3",
19 | "superagent": "1.8.3",
20 | "superagent-bluebird-promise": "3.0.0"
21 | },
22 | "devDependencies": {
23 | "babel-core": "6.8.0",
24 | "babel-loader": "6.2.4",
25 | "babel-plugin-react-transform": "2.0.2",
26 | "babel-plugin-transform-object-rest-spread": "6.8.0",
27 | "babel-plugin-transform-runtime": "6.8.0",
28 | "babel-preset-es2015": "6.6.0",
29 | "babel-preset-react": "6.5.0",
30 | "babel-register": "6.8.0",
31 | "babel-runtime": "6.6.1",
32 | "compression-webpack-plugin": "0.2.0",
33 | "css-loader": "0.23.1",
34 | "eslint": "2.9.0",
35 | "eslint-plugin-lodash": "1.8.4",
36 | "eslint-plugin-react": "5.1.1",
37 | "extract-text-webpack-plugin": "1.0.1",
38 | "file-loader": "0.8.5",
39 | "grunt": "1.0.1",
40 | "grunt-aws": "0.6.2",
41 | "grunt-contrib-clean": "1.0.0",
42 | "grunt-eslint": "18.1.0",
43 | "grunt-jscs": "2.8.0",
44 | "grunt-mocha-test": "0.12.7",
45 | "grunt-webpack": "1.0.11",
46 | "html-webpack-plugin": "2.19.0",
47 | "jscs": "2.9.0",
48 | "jsdom": "9.0.0",
49 | "json-loader": "0.5.4",
50 | "less": "2.7.1",
51 | "less-loader": "2.2.3",
52 | "mocha": "2.4.5",
53 | "react-addons-test-utils": "15.0.2",
54 | "react-hot-loader": "1.3.0",
55 | "react-transform-catch-errors": "1.0.2",
56 | "react-transform-hmr": "1.0.4",
57 | "redbox-react": "1.2.4",
58 | "sinon": "1.17.4",
59 | "style-loader": "0.13.1",
60 | "unexpected": "10.13.2",
61 | "unexpected-moment": "1.0.2",
62 | "unexpected-sinon": "10.2.0",
63 | "url-loader": "0.5.7",
64 | "webpack": "1.13.0",
65 | "webpack-dev-server": "1.14.1"
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/src/app/ConfigurationStore.js:
--------------------------------------------------------------------------------
1 | /*globals require, process */
2 | import _ from "lodash";
3 | import sjcl from "sjcl";
4 | import request from "superagent-bluebird-promise";
5 | import CachingStore from "../flux/CachingStore";
6 | import Logger from "../util/Logger";
7 | import Message from "../source/Message";
8 | import SOURCE_TYPES from "../source/SourceTypes";
9 |
10 | var log = new Logger(__filename);
11 |
12 | window.encrypt = (password, data) => {
13 | log.info("Encrypted value:", JSON.stringify(sjcl.encrypt(password, data)));
14 | };
15 |
16 | export default class ConfigurationStore extends CachingStore {
17 | constructor(configFileName = "config.json") {
18 | super(__filename);
19 | this.state = _.defaults(this.getCachedState() || {}, {
20 | sources: [],
21 | panels: {},
22 | password: null,
23 | passwordNeeded: false
24 | });
25 | this.configFileName = configFileName;
26 |
27 | this._fetchConfig();
28 | }
29 |
30 | onChanged(listener) {
31 | return this._registerListener("sources", listener);
32 | }
33 |
34 | getSources() {
35 | return this.state.sources;
36 | }
37 |
38 | getPanels() {
39 | return this.state.panels;
40 | }
41 |
42 | getPassword() {
43 | return this.state.password;
44 | }
45 |
46 | isPasswordNeeded() {
47 | return this.state.passwordNeeded;
48 | }
49 |
50 | setPassword(newPass) {
51 | this.setState({password: newPass});
52 | this.saveToLocalStorage();
53 | this._fetchConfig();
54 | }
55 |
56 | decrypt(data, password) {
57 | if (_.isString(data)) {
58 | return data;
59 | }
60 | if (!password) {
61 | this.setState({passwordNeeded: true});
62 | this._trigger("sources");
63 | return null;
64 | }
65 |
66 | try {
67 | return sjcl.decrypt(password, data.encrypted);
68 | } catch (e) {
69 | this.setState({
70 | password: null,
71 | passwordNeeded: true
72 | });
73 | log.error("Password incorrect.", e);
74 | return null;
75 | }
76 | }
77 |
78 | marshalState() {
79 | return {
80 | password: this.state.password
81 | };
82 | }
83 |
84 | unmarshalState(data) {
85 | return {
86 | password: data.password
87 | };
88 | }
89 |
90 | _createSource(sourceConfig, def1, def2) {
91 | var type = sourceConfig.type;
92 | var SourceType = _.find(SOURCE_TYPES, {type: type});
93 | if (!SourceType) {
94 | var err = "Unknown source type '" + type + "' in configuration.";
95 | log.error(err);
96 | return new Message({
97 | title: sourceConfig.title,
98 | status: "warning",
99 | message: err
100 | });
101 | }
102 | var defaults = _.defaults(
103 | def1[type] || {},
104 | def1.all || {},
105 | def2[type] || {},
106 | def2.all || {}
107 | );
108 |
109 | return new SourceType(_.defaults(sourceConfig, defaults), {
110 | decrypt: encrypted => this.decrypt(encrypted, this.state.password)
111 | });
112 | }
113 |
114 | _parseConfig(config) {
115 | var panels = {};
116 | var sources = [];
117 |
118 | var createPanel = (panel, index) => {
119 | if (!panels[index]) {
120 | panels[index] = {
121 | title: panel.title,
122 | sources: []
123 | };
124 | }
125 |
126 | _.forEach(panel.sources, sourceConfig => {
127 | var source = this._createSource(sourceConfig, panel.defaults || {}, config.defaults || {});
128 | sources.push(source);
129 |
130 | panels[index].sources.push(source);
131 | });
132 | };
133 |
134 | if (config.panels) {
135 | _.forEach(config.panels, createPanel);
136 |
137 | if (config.sources) {
138 | log.warn("Both 'panels' and 'sources' were specified at top level in configuration, ignoring 'sources'.");
139 | }
140 | } else {
141 | createPanel(config, 0);
142 | }
143 |
144 | this.setState({
145 | sources: sources,
146 | panels: panels
147 | });
148 | }
149 |
150 | _fetchConfig() {
151 | let jsonPromise;
152 | if (process.env.NODE_ENV !== "production") {
153 | jsonPromise = Promise.resolve(require("../config.json"));
154 | } else {
155 | jsonPromise = request.get(this.configFileName)
156 | .promise()
157 | .catch(e => {
158 | log.error("Configuration file '" + this.configFileName + "' not found.", e);
159 | throw e;
160 | })
161 | .then(response => {
162 | return JSON.parse(response.text);
163 | });
164 | }
165 | return jsonPromise
166 | .then(json => {
167 | this._parseConfig(json);
168 | })
169 | .catch(e => log.error("Unable to load configuration", e));
170 | }
171 | }
--------------------------------------------------------------------------------
/src/app/PasswordPrompt.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import PureRenderMixin from "react-addons-pure-render-mixin";
3 | import LinkedStateMixin from "react-addons-linked-state-mixin";
4 | import {Glyphicon, Modal, FormControl, Button, Panel} from "react-bootstrap";
5 | import Mixins from "../util/Mixins";
6 |
7 | export default class PasswordPrompt extends React.Component {
8 | constructor(props) {
9 | super(props);
10 | this.state = {
11 | showModal: false,
12 | password: ""
13 | };
14 |
15 | this.props.configStore.onChanged(() => this.forceUpdate());
16 | }
17 |
18 | savePassword(e) {
19 | e.preventDefault();
20 | this.props.configStore.setPassword(this.state.password);
21 | this.setState({
22 | showModal: false
23 | });
24 | }
25 |
26 | deletePassword() {
27 | this.props.configStore.setPassword(null);
28 | this.setState({
29 | showModal: false
30 | });
31 | }
32 |
33 | renderModalBody() {
34 | if (this.props.configStore.getPassword()) {
35 | return (
36 | A password for unlocking the configuration file has been saved. The configuration file contains sensitive keys or passwords that have been encrypted. Entering the password will unlock them, and save the password on this computer for the future.
{message.message}
38 |