├── .dockerignore ├── .env.example ├── .github ├── dependabot.yml └── workflows │ └── nodejs.yml ├── .gitignore ├── Dockerfile ├── LICENSE.md ├── README.md ├── auth.js ├── components ├── badge.js ├── circle-progress.js ├── counter.js ├── dashboard.js ├── error-icon.js ├── link.js ├── loading-indicator.js ├── table.js ├── widget.js └── widgets │ ├── bitbucket │ └── pull-request-count.js │ ├── datetime │ └── index.js │ ├── elasticsearch │ └── hit-count.js │ ├── github │ └── issue-count.js │ ├── jenkins │ ├── build-duration.js │ ├── job-health.js │ └── job-status.js │ ├── jira │ ├── issue-count.js │ └── sprint-days-remaining.js │ ├── pagespeed-insights │ ├── score.js │ └── stats.js │ ├── sonarqube │ └── index.js │ └── title │ └── index.js ├── lib └── auth.js ├── next.config.js ├── package.json ├── pages ├── _document.js └── index.js ├── public └── static │ └── favicon.png ├── styles ├── dark-theme.js └── light-theme.js └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .github 3 | components 4 | node_modules 5 | pages 6 | styles 7 | .dockerignore 8 | .DS_Store 9 | .gitignore 10 | Dockerfile 11 | LICENSE.md 12 | npm-debug.log 13 | README.md 14 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | BITBUCKET_USER= 2 | BITBUCKET_PASS= 3 | ELASTICSEARCH_USER= 4 | ELASTICSEARCH_PASS= 5 | JENKINS_USER= 6 | JENKINS_PASS= 7 | JIRA_USER= 8 | JIRA_PASS= 9 | SONARQUBE_USER= 10 | SONARQUBE_PASS= 11 | GITHUB_USER= 12 | GITHUB_PASS= 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "04:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [10.x, 12.x, 13.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: yarn install, build, and test 21 | run: | 22 | yarn install 23 | yarn build 24 | yarn test 25 | env: 26 | CI: true 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # Next.js directory 61 | .next 62 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine 2 | 3 | # Create app directory 4 | RUN mkdir -p /usr/src/app 5 | WORKDIR /usr/src/app 6 | 7 | # Set environment variable 8 | ENV NODE_ENV production 9 | 10 | # Install app dependencies 11 | COPY package.json yarn.lock /usr/src/app/ 12 | RUN yarn --pure-lockfile && yarn cache clean 13 | 14 | # Bundle app source 15 | COPY . /usr/src/app 16 | 17 | # Port 18 | EXPOSE 3000 19 | 20 | # Start 21 | CMD [ "yarn", "start" ] 22 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-present Daniel Bayerlein 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Dashboard 3 |

4 | 5 |

6 | Dashboard 7 |

8 | 9 |

10 | Create your own team dashboard with custom widgets. 11 |

12 | 13 |

14 | 15 | Actions Status 16 | 17 | 18 | JavaScript Style Guide 19 | 20 | 21 | Dependabot Status 22 | 23 | 24 | Deploy to now 25 | 26 |

27 | 28 | ## Table of Contents 29 | 30 | * [Installation](#installation) 31 | * [Server](#server) 32 | * [Development](#development) 33 | * [Production](#production) 34 | * [Docker](#docker) 35 | * [Create a Dashboard](#create-a-dashboard) 36 | * [Available Widgets](#available-widgets) 37 | * [DateTime](#datetime) 38 | * [Jenkins Job Status](#jenkins-job-status) 39 | * [Jenkins Job Health](#jenkins-job-health) 40 | * [Jenkins Build Duration](#jenkins-build-duration) 41 | * [JIRA Issue Count](#jira-issue-count) 42 | * [JIRA Sprint Days Remaining](#jira-sprint-days-remaining) 43 | * [Bitbucket PullRequest Count](#bitbucket-pullrequest-count) 44 | * [PageSpeed Insights Score](#pagespeed-insights-score) 45 | * [PageSpeed Insights Stats](#pagespeed-insights-stats) 46 | * [SonarQube](#sonarqube) 47 | * [Elasticsearch Hit Count](#elasticsearch-hit-count) 48 | * [GitHub Issue Count](#github-issue-count) 49 | * [Title](#title) 50 | * [Available Themes](#available-themes) 51 | * [light](#light) 52 | * [dark](#dark) 53 | * [Authentication](#authentication) 54 | * [Cross-Origin Resource Sharing (CORS)](#cross-origin-resource-sharing-cors) 55 | * [Proxy](#proxy) 56 | * [Resources](#resources) 57 | * [License](#license) 58 | 59 | ## Installation 60 | 61 | 1. [Download](../../archive/master.zip) or clone the repository. 62 | 2. Install the dependencies with `npm install`. 63 | 64 | ## Server 65 | 66 | ### Development 67 | 68 | Run `npm run dev` and go to http://localhost:3000. 69 | 70 | ### Production 71 | 72 | Build your dashboard for production with `npm run build` and then start the 73 | server with `npm start`. 74 | 75 | ### Docker 76 | 77 | 1. Build your dashboard for production with `npm run build` 78 | 2. Build the image with `docker build -t dashboard .` 79 | 3. Start the container with `docker run -d -p 8080:3000 dashboard` 80 | 4. Go to http://localhost:8080 81 | 82 | ## Create a Dashboard 83 | 84 | You can create multiple dashboards. 85 | For example populate `pages/team-unicorn.js` inside your project: 86 | 87 | ```javascript 88 | import Dashboard from '../components/dashboard' 89 | import DateTime from '../components/widgets/datetime' 90 | import lightTheme from '../styles/light-theme' 91 | 92 | export default () => ( 93 | 94 | 95 | 96 | ) 97 | ``` 98 | 99 | This dashboard is available at http://localhost:3000/team-unicorn. 100 | 101 | For an example, see [pages/index.js](./pages/index.js). 102 | 103 | ## Available Widgets 104 | 105 | ### [DateTime](./components/widgets/datetime/index.js) 106 | 107 | #### Example 108 | 109 | ```javascript 110 | import DateTime from '../components/widgets/datetime' 111 | 112 | 113 | ``` 114 | 115 | #### props 116 | 117 | * `interval`: Refresh interval in milliseconds (Default: `10000`) 118 | 119 | ### [Jenkins Job Status](./components/widgets/jenkins/job-status.js) 120 | 121 | #### Example 122 | 123 | ```javascript 124 | import JenkinsJobStatus from '../components/widgets/jenkins/job-status' 125 | 126 | 133 | ``` 134 | 135 | For Jenkins multibranch projects add `branch` to the object. 136 | 137 | #### props 138 | 139 | * `title`: Widget title (Default: `Job Status`) 140 | * `interval`: Refresh interval in milliseconds (Default: `300000`) 141 | * `url`: Jenkins URL 142 | * `jobs`: List of all jobs 143 | * `authKey`: Credential key, defined in [auth.js](./auth.js) 144 | 145 | ### [Jenkins Job Health](./components/widgets/jenkins/job-health.js) 146 | 147 | #### Example 148 | 149 | ```javascript 150 | import JenkinsJobHealth from '../components/widgets/jenkins/job-health' 151 | 152 | 159 | ``` 160 | 161 | For Jenkins multibranch projects add `branch` to the object. 162 | 163 | #### props 164 | 165 | * `title`: Widget title (Default: `Job Health`) 166 | * `interval`: Refresh interval in milliseconds (Default: `300000`) 167 | * `url`: Jenkins URL 168 | * `jobs`: List of all jobs 169 | * `authKey`: Credential key, defined in [auth.js](./auth.js) 170 | 171 | 172 | ### [Jenkins Build Duration](./components/widgets/jenkins/build-duration.js) 173 | 174 | #### Example 175 | 176 | ```javascript 177 | import JenkinsBuildDuration from '../components/widgets/jenkins/build-duration' 178 | 179 | 186 | ``` 187 | 188 | For Jenkins multibranch projects add `branch` to the object. 189 | 190 | #### props 191 | 192 | * `title`: Widget title (Default: `Build Duration`) 193 | * `interval`: Refresh interval in milliseconds (Default: `300000`) 194 | * `url`: Jenkins URL 195 | * `jobs`: List of all jobs 196 | * `authKey`: Credential key, defined in [auth.js](./auth.js) 197 | 198 | ### [JIRA Issue Count](./components/widgets/jira/issue-count.js) 199 | 200 | #### Example 201 | 202 | ```javascript 203 | import JiraIssueCount from '../components/widgets/jira/issue-count' 204 | 205 | 210 | ``` 211 | 212 | For Jenkins multibranch projects add `branch` to the object. 213 | 214 | #### props 215 | 216 | * `title`: Widget title (Default: `JIRA Issue Count`) 217 | * `interval`: Refresh interval in milliseconds (Default: `300000`) 218 | * `url`: JIRA Server URL 219 | * `query`: JIRA search query (`jql`) 220 | * `authKey`: Credential key, defined in [auth.js](./auth.js) 221 | 222 | ### [JIRA Sprint Days Remaining](./components/widgets/jira/sprint-days-remaining.js) 223 | 224 | #### Example 225 | 226 | ```javascript 227 | import JiraSprintDaysRemaining from '../components/widgets/jira/sprint-days-remaining' 228 | 229 | 234 | ``` 235 | 236 | #### props 237 | 238 | * `title`: Widget title (Default: `JIRA Sprint Days Remaining`) 239 | * `interval`: Refresh interval in milliseconds (Default: `3600000`) 240 | * `url`: JIRA Server URL 241 | * `boardId`: JIRA board id 242 | * `authKey`: Credential key, defined in [auth.js](./auth.js) 243 | 244 | ### [Bitbucket PullRequest Count](./components/widgets/bitbucket/pull-request-count.js) 245 | 246 | #### Example 247 | 248 | ```javascript 249 | import BitbucketPullRequestCount from '../components/widgets/bitbucket/pull-request-count' 250 | 251 | 258 | ``` 259 | 260 | #### props 261 | 262 | * `title`: Widget title (Default: `Bitbucket PR Count`) 263 | * `interval`: Refresh interval in milliseconds (Default: `300000`) 264 | * `url`: Bitbucket Server URL 265 | * `project`: Bitbucket project key 266 | * `repository`: Bitbucket repository slug 267 | * `users`: Bitbucket user slugs as an array 268 | * `authKey`: Credential key, defined in [auth.js](./auth.js) 269 | 270 | ### [PageSpeed Insights Score](./components/widgets/pagespeed-insights/score.js) 271 | 272 | #### Example 273 | 274 | ```javascript 275 | import PageSpeedInsightsScore from '../components/widgets/pagespeed-insights/score' 276 | 277 | 278 | ``` 279 | 280 | #### props 281 | 282 | * `title`: Widget title (Default: `PageSpeed Score`) 283 | * `interval`: Refresh interval in milliseconds (Default: `43200000`) 284 | * `url`: URL to fetch and analyze 285 | * `strategy`: Analysis strategy (Default: `desktop`) 286 | * Acceptable values: `desktop` | `mobile` 287 | * `filterThirdPartyResources`: Indicates if third party resources should be filtered out (Default: `false`) 288 | 289 | ### [PageSpeed Insights Stats](./components/widgets/pagespeed-insights/stats.js) 290 | 291 | #### Example 292 | 293 | ```javascript 294 | import PageSpeedInsightsStats from '../components/widgets/pagespeed-insights/stats' 295 | 296 | 297 | ``` 298 | 299 | #### props 300 | 301 | * `title`: Widget title (Default: `PageSpeed Stats`) 302 | * `interval`: Refresh interval in milliseconds (Default: `43200000`) 303 | * `url`: URL to fetch and analyze 304 | * `strategy`: Analysis strategy (Default: `desktop`) 305 | * Acceptable values: `desktop` | `mobile` 306 | * `filterThirdPartyResources`: Indicates if third party resources should be filtered out (Default: `false`) 307 | 308 | ### [SonarQube](./components/widgets/sonarqube/index.js) 309 | 310 | #### Example 311 | 312 | ```javascript 313 | import SonarQube from '../components/widgets/sonarqube' 314 | 315 | 319 | ``` 320 | 321 | #### props 322 | 323 | * `title`: Widget title (Default: `SonarQube`) 324 | * `interval`: Refresh interval in milliseconds (Default: `300000`) 325 | * `url`: SonarQube URL 326 | * `componentKey`: SonarQube project key 327 | * `authKey`: Credential key, defined in [auth.js](./auth.js) 328 | 329 | ### [Elasticsearch Hit Count](./components/widgets/elasticsearch/hit-count.js) 330 | 331 | #### Example 332 | 333 | ```javascript 334 | import ElasticsearchHitCount from '../components/widgets/elasticsearch/hit-count' 335 | 336 | 342 | ``` 343 | 344 | #### props 345 | 346 | * `title`: Widget title (Default: `Elasticsearch Hit Count`) 347 | * `interval`: Refresh interval in milliseconds (Default: `300000`) 348 | * `url`: Elasticsearch URL 349 | * `index`: Elasticsearch index to search in 350 | * `query`: Elasticsearch query 351 | * `authKey`: Credential key, defined in [auth.js](./auth.js) 352 | 353 | ### [GitHub Issue Count](./components/widgets/github/issue-count.js) 354 | 355 | #### Example 356 | 357 | ```javascript 358 | import GitHubIssueCount from '../components/widgets/github/issue-count' 359 | 360 | 364 | ``` 365 | 366 | #### props 367 | 368 | * `title`: Widget title (Default: `GitHub Issue Count`) 369 | * `interval`: Refresh interval in milliseconds (Default: `300000`) 370 | * `owner`: Owner of the repository 371 | * `repository`: Name of the repository 372 | * `authKey`: Credential key, defined in [auth.js](./auth.js) 373 | 374 | ### [Title](./components/widgets/title/index.js) 375 | 376 | #### Example 377 | 378 | ```javascript 379 | import Title from '../components/widgets/title' 380 | 381 | API Status 382 | ``` 383 | 384 | ## Available Themes 385 | 386 | ### [light](./styles/light-theme.js) 387 | 388 | #### Example 389 | 390 | ```javascript 391 | import lightTheme from '../styles/light-theme' 392 | 393 | 394 | ... 395 | 396 | ``` 397 | 398 | #### Preview 399 | 400 | ![dashboard-light](https://cloud.githubusercontent.com/assets/457834/26214930/8c065dce-3bfe-11e7-9da0-2d6ebba2dfb8.png) 401 | 402 | ### [dark](./styles/dark-theme.js) 403 | 404 | #### Example 405 | 406 | ```javascript 407 | import darkTheme from '../styles/dark-theme' 408 | 409 | 410 | ... 411 | 412 | ``` 413 | 414 | #### Preview 415 | 416 | ![dashboard-dark](https://cloud.githubusercontent.com/assets/457834/26214954/a668dc50-3bfe-11e7-8c19-7a0c7dd260e7.png) 417 | 418 | ## Authentication 419 | 420 | Any widget can authenticate itself, should your server expect this. We use 421 | [basic authentication](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). 422 | 423 | 1. Define your credential key in [auth.js](./auth.js). For example: 424 | ```javascript 425 | jira: { 426 | username: process.env.JIRA_USER, 427 | password: process.env.JIRA_PASS 428 | } 429 | ``` 430 | 2. Give the defined credential key `jira` via prop `authKey` to the widget. 431 | For example: 432 | ```javascript 433 | 438 | ``` 439 | 3. Create a `.env` file or rename `.env.example` to `.env` in the root directory of your project. Add 440 | environment-specific variables on new lines in the form of `NAME=VALUE`. 441 | For example: 442 | ```dosini 443 | JIRA_USER=root 444 | JIRA_PASS=s1mpl3 445 | ``` 446 | 447 | ## Cross-Origin Resource Sharing (CORS) 448 | 449 | [Cross-Origin Resource Sharing](https://www.w3.org/TR/cors/) (CORS) is a W3C 450 | spec that allows cross-domain communication from the browser. By building on 451 | top of the XMLHttpRequest object, CORS allows developers to work with the same 452 | idioms as same-domain requests. 453 | 454 | ### Proxy 455 | 456 | You can use a proxy (e.g. [hapi-rest-proxy](https://github.com/chrishelgert/hapi-rest-proxy)) 457 | to enable CORS request for any website. 458 | 459 | #### Server 460 | 461 | ```bash 462 | docker pull chrishelgert/hapi-rest-proxy 463 | docker run -d -p 3001:8080 chrishelgert/hapi-rest-proxy 464 | ``` 465 | 466 | #### Dashboard 467 | 468 | ```javascript 469 | 473 | ``` 474 | 475 | ### Resources 476 | 477 | * https://www.w3.org/TR/cors/ 478 | * https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS 479 | * https://enable-cors.org 480 | * https://en.wikipedia.org/wiki/Cross-origin_resource_sharing 481 | 482 | ## License 483 | 484 | Copyright (c) 2017-present Daniel Bayerlein. See [LICENSE](./LICENSE.md) for details. 485 | -------------------------------------------------------------------------------- /auth.js: -------------------------------------------------------------------------------- 1 | export default { 2 | bitbucket: { 3 | username: process.env.BITBUCKET_USER, 4 | password: process.env.BITBUCKET_PASS 5 | }, 6 | elasticsearch: { 7 | username: process.env.ELASTICSEARCH_USER, 8 | password: process.env.ELASTICSEARCH_PASS 9 | }, 10 | jenkins: { 11 | username: process.env.JENKINS_USER, 12 | password: process.env.JENKINS_PASS 13 | }, 14 | jira: { 15 | username: process.env.JIRA_USER, 16 | password: process.env.JIRA_PASS 17 | }, 18 | sonarqube: { 19 | username: process.env.SONARQUBE_USER, 20 | password: process.env.SONARQUBE_PASS 21 | }, 22 | github: { 23 | username: process.env.GITHUB_USER, 24 | password: process.env.GITHUB_PASS 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /components/badge.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { size } from 'polished' 3 | 4 | export default styled.span` 5 | ${size('1.75em')} 6 | background-color: transparent; 7 | border-radius: 50%; 8 | color: ${props => props.theme.palette.textInvertColor}; 9 | display: inline-block; 10 | line-height: 1.75em; 11 | text-align: center; 12 | ` 13 | -------------------------------------------------------------------------------- /components/circle-progress.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { size } from 'polished' 3 | 4 | const Svg = styled.svg` 5 | ${size('14em')} 6 | fill: transparent; 7 | margin: auto; 8 | ` 9 | 10 | const Circle = styled.circle` 11 | stroke-linecap: round; 12 | stroke-width: 10; 13 | transform: translate(100px, 100px) rotate(-89.9deg); 14 | transition: stroke-dashoffset 0.3s linear; 15 | 16 | &.background { 17 | stroke: ${props => props.theme.palette.borderColor}; 18 | } 19 | 20 | &.progress { 21 | stroke: ${props => props.theme.palette.primaryColor}; 22 | } 23 | ` 24 | 25 | const Text = styled.text` 26 | fill: ${props => props.theme.palette.textColor}; 27 | font-size: 4em; 28 | text-anchor: middle; 29 | ` 30 | 31 | export default ({ max = 100, radius = 90, unit = '', value }) => { 32 | const strokeDasharray = 2 * radius * Math.PI 33 | const strokeDashoffset = ((max - value) / max) * strokeDasharray 34 | 35 | return ( 36 | 37 | 38 | 44 | {value}{unit} 45 | 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /components/counter.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const Counter = styled.div` 4 | font-size: 4em; 5 | color: ${props => props.theme.palette.accentColor}; 6 | ` 7 | 8 | export default ({ value }) => ( 9 | {value} 10 | ) 11 | -------------------------------------------------------------------------------- /components/dashboard.js: -------------------------------------------------------------------------------- 1 | import Head from 'next/head' 2 | import styled, { createGlobalStyle, ThemeProvider } from 'styled-components' 3 | import { normalize } from 'polished' 4 | 5 | const GlobalStyle = createGlobalStyle` 6 | ${normalize()} 7 | 8 | html { 9 | font-family: 'Roboto', sans-serif; 10 | } 11 | ` 12 | 13 | const Container = styled.main` 14 | align-content: center; 15 | align-items: center; 16 | background-color: ${props => props.theme.palette.backgroundColor}; 17 | color: ${props => props.theme.palette.textColor}; 18 | display: flex; 19 | flex-flow: row wrap; 20 | justify-content: center; 21 | min-height: 100vh; 22 | ` 23 | 24 | export default ({ children, theme, title = 'Dashboard' }) => ( 25 | 26 | 27 | 28 | {title} 29 | 30 | 34 | 35 | 36 | {children} 37 | 38 | 39 | 40 | 41 | ) 42 | -------------------------------------------------------------------------------- /components/error-icon.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { size } from 'polished' 3 | 4 | const Svg = styled.svg` 5 | ${size('5em')} 6 | fill: ${props => props.theme.palette.errorColor}; 7 | ` 8 | 9 | export default () => ( 10 | 11 | 12 | 13 | 14 | ) 15 | -------------------------------------------------------------------------------- /components/link.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export default styled.a` 4 | text-decoration: none; 5 | color: ${props => props.theme.palette.textColor}; 6 | ` 7 | -------------------------------------------------------------------------------- /components/loading-indicator.js: -------------------------------------------------------------------------------- 1 | import styled, { keyframes } from 'styled-components' 2 | 3 | const rotation = keyframes` 4 | 0% { 5 | transform: rotate(0deg); 6 | } 7 | 8 | 100% { 9 | transform: rotate(270deg); 10 | } 11 | ` 12 | 13 | const turn = keyframes` 14 | 0% { 15 | stroke-dashoffset: 187; 16 | } 17 | 18 | 50% { 19 | stroke-dashoffset: 46.75; 20 | transform: rotate(135deg); 21 | } 22 | 23 | 100% { 24 | stroke-dashoffset: 187; 25 | transform: rotate(450deg); 26 | } 27 | ` 28 | 29 | const Svg = styled.svg` 30 | animation: ${rotation} 1.4s linear infinite; 31 | height: ${props => props.size}; 32 | width: ${props => props.size}; 33 | ` 34 | 35 | const Circle = styled.circle` 36 | animation: ${turn} 1.4s ease-in-out infinite; 37 | fill: none; 38 | stroke: ${props => props.theme.palette.primaryColor}; 39 | stroke-dasharray: 187; 40 | stroke-dashoffset: 0; 41 | stroke-linecap: round; 42 | stroke-width: 6; 43 | transform-origin: center; 44 | ` 45 | 46 | export default ({ size = 'medium' }) => { 47 | const svgSize = size === 'small' ? '1.75em' : '5em' 48 | 49 | return ( 50 | 51 | 52 | 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /components/table.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | export default styled.table` 4 | border-spacing: 0.75em; 5 | ` 6 | 7 | export const Th = styled.th` 8 | text-align: right; 9 | ` 10 | 11 | export const Td = styled.td` 12 | text-align: left; 13 | ` 14 | -------------------------------------------------------------------------------- /components/widget.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | import { size } from 'polished' 3 | import LoadingIndicator from './loading-indicator' 4 | import ErrorIcon from './error-icon' 5 | 6 | const Container = styled.div` 7 | ${size('20em')} 8 | align-items: center; 9 | background-color: ${props => props.theme.palette.canvasColor}; 10 | border: 1px solid ${props => props.theme.palette.borderColor}; 11 | display: flex; 12 | flex-direction: column; 13 | justify-content: center; 14 | margin: 1em; 15 | padding: 1em; 16 | ` 17 | 18 | const Title = styled.h1` 19 | text-align: center; 20 | ` 21 | 22 | export default ({ children, error = false, loading = false, title = '' }) => { 23 | let content 24 | 25 | if (loading) { 26 | content = 27 | } else if (error) { 28 | content = 29 | } else { 30 | content =
{children}
31 | } 32 | 33 | return ( 34 | 35 | {title ? {title} : ''} 36 | {content} 37 | 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /components/widgets/bitbucket/pull-request-count.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | import fetch from 'isomorphic-unfetch' 3 | import { object, string, number, array } from 'yup' 4 | import Widget from '../../widget' 5 | import Counter from '../../counter' 6 | import { basicAuthHeader } from '../../../lib/auth' 7 | 8 | const schema = object().shape({ 9 | url: string().url().required(), 10 | project: string().required(), 11 | repository: string().required(), 12 | interval: number(), 13 | title: string(), 14 | users: array().of(string()), 15 | authKey: string() 16 | }) 17 | 18 | export default class BitbucketPullRequestCount extends Component { 19 | static defaultProps = { 20 | interval: 1000 * 60 * 5, 21 | title: 'Bitbucket PR Count', 22 | users: [] 23 | } 24 | 25 | state = { 26 | count: 0, 27 | error: false, 28 | loading: true 29 | } 30 | 31 | componentDidMount () { 32 | schema.validate(this.props) 33 | .then(() => this.fetchInformation()) 34 | .catch((err) => { 35 | console.error(`${err.name} @ ${this.constructor.name}`, err.errors) 36 | this.setState({ error: true, loading: false }) 37 | }) 38 | } 39 | 40 | componentWillUnmount () { 41 | clearTimeout(this.timeout) 42 | } 43 | 44 | async fetchInformation () { 45 | const { authKey, url, project, repository, users } = this.props 46 | const opts = authKey ? { headers: basicAuthHeader(authKey) } : {} 47 | 48 | try { 49 | const res = await fetch(`${url}/rest/api/1.0/projects/${project}/repos/${repository}/pull-requests?limit=100`, opts) 50 | const json = await res.json() 51 | 52 | let count 53 | if (users.length) { 54 | count = json.values.filter((el) => users.includes(el.user.slug)).length 55 | } else { 56 | count = json.size 57 | } 58 | 59 | this.setState({ count, error: false, loading: false }) 60 | } catch (error) { 61 | this.setState({ error: true, loading: false }) 62 | } finally { 63 | this.timeout = setTimeout(() => this.fetchInformation(), this.props.interval) 64 | } 65 | } 66 | 67 | render () { 68 | const { count, error, loading } = this.state 69 | const { title } = this.props 70 | return ( 71 | 72 | 73 | 74 | ) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /components/widgets/datetime/index.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | import tinytime from 'tinytime' 3 | import styled from 'styled-components' 4 | import Widget from '../../widget' 5 | 6 | const TimeItem = styled.div` 7 | font-size: 4em; 8 | text-align: center; 9 | ` 10 | 11 | const DateItem = styled.div` 12 | font-size: 1.5em; 13 | text-align: center; 14 | ` 15 | 16 | export default class DateTime extends Component { 17 | static defaultProps = { 18 | interval: 1000 * 10 19 | } 20 | 21 | state = { 22 | date: new Date() 23 | } 24 | 25 | componentDidMount () { 26 | const { interval } = this.props 27 | 28 | this.interval = setInterval(() => this.updateTime(), interval) 29 | } 30 | 31 | updateTime () { 32 | this.setState({ date: new Date() }) 33 | } 34 | 35 | componentWillUnmount () { 36 | clearInterval(this.interval) 37 | } 38 | 39 | render () { 40 | const { date } = this.state 41 | return ( 42 | 43 | {tinytime('{H}:{mm}').render(date)} 44 | {tinytime('{DD}.{Mo}.{YYYY}').render(date)} 45 | 46 | ) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /components/widgets/elasticsearch/hit-count.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | import fetch from 'isomorphic-unfetch' 3 | import { object, string, number } from 'yup' 4 | import Widget from '../../widget' 5 | import Counter from '../../counter' 6 | import { basicAuthHeader } from '../../../lib/auth' 7 | 8 | const schema = object().shape({ 9 | url: string().url().required(), 10 | index: string().required(), 11 | query: string().required(), 12 | interval: number(), 13 | title: string() 14 | }) 15 | 16 | export default class ElasticsearchHitCount extends Component { 17 | static defaultProps = { 18 | interval: 1000 * 60 * 5, 19 | title: 'Elasticsearch Hit Count' 20 | } 21 | 22 | state = { 23 | count: 0, 24 | error: false, 25 | loading: true 26 | } 27 | 28 | componentDidMount () { 29 | schema.validate(this.props) 30 | .then(() => this.fetchInformation()) 31 | .catch((err) => { 32 | console.error(`${err.name} @ ${this.constructor.name}`, err.errors) 33 | this.setState({ error: true, loading: false }) 34 | }) 35 | } 36 | 37 | componentWillUnmount () { 38 | clearTimeout(this.timeout) 39 | } 40 | 41 | async fetchInformation () { 42 | const { authKey, index, query, url } = this.props 43 | const opts = authKey ? { headers: basicAuthHeader(authKey) } : {} 44 | 45 | try { 46 | const res = await fetch(`${url}/${index}/_search?q=${query}`, opts) 47 | const json = await res.json() 48 | 49 | this.setState({ count: json.hits.total, error: false, loading: false }) 50 | } catch (error) { 51 | this.setState({ error: true, loading: false }) 52 | } finally { 53 | this.timeout = setTimeout(() => this.fetchInformation(), this.props.interval) 54 | } 55 | } 56 | 57 | render () { 58 | const { count, error, loading } = this.state 59 | const { title } = this.props 60 | return ( 61 | 62 | 63 | 64 | ) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /components/widgets/github/issue-count.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | import fetch from 'isomorphic-unfetch' 3 | import { object, string, number } from 'yup' 4 | import Widget from '../../widget' 5 | import Counter from '../../counter' 6 | import { basicAuthHeader } from '../../../lib/auth' 7 | 8 | const schema = object().shape({ 9 | owner: string().required(), 10 | repository: string().required(), 11 | interval: number(), 12 | title: string(), 13 | authKey: string() 14 | }) 15 | 16 | export default class GitHubIssueCount extends Component { 17 | static defaultProps = { 18 | interval: 1000 * 60 * 5, 19 | title: 'GitHub Issue Count' 20 | } 21 | 22 | state = { 23 | count: 0, 24 | error: false, 25 | loading: true 26 | } 27 | 28 | componentDidMount () { 29 | schema.validate(this.props) 30 | .then(() => this.fetchInformation()) 31 | .catch((err) => { 32 | console.error(`${err.name} @ ${this.constructor.name}`, err.errors) 33 | this.setState({ error: true, loading: false }) 34 | }) 35 | } 36 | 37 | componentWillUnmount () { 38 | clearTimeout(this.timeout) 39 | } 40 | 41 | async fetchInformation () { 42 | const { authKey, owner, repository } = this.props 43 | const opts = authKey ? { headers: basicAuthHeader(authKey) } : {} 44 | 45 | try { 46 | const res = await fetch(`https://api.github.com/repos/${owner}/${repository}`, opts) 47 | const json = await res.json() 48 | 49 | this.setState({ count: json.open_issues_count, error: false, loading: false }) 50 | } catch (error) { 51 | this.setState({ error: true, loading: false }) 52 | } finally { 53 | this.timeout = setTimeout(() => this.fetchInformation(), this.props.interval) 54 | } 55 | } 56 | 57 | render () { 58 | const { count, error, loading } = this.state 59 | const { title } = this.props 60 | return ( 61 | 62 | 63 | 64 | ) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /components/widgets/jenkins/build-duration.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | import fetch from 'isomorphic-unfetch' 3 | import styled from 'styled-components' 4 | import { object, string, array, number } from 'yup' 5 | 6 | import Widget from '../../widget' 7 | import Link from '../../link' 8 | import Table, { Th, Td } from '../../table' 9 | import LoadingIndicator from '../../loading-indicator' 10 | import { basicAuthHeader } from '../../../lib/auth' 11 | 12 | const Kpi = styled.span` 13 | color: ${props => props.theme.palette.primaryColor}; 14 | font-weight: 700; 15 | font-size: 20px; 16 | ` 17 | 18 | const schema = object().shape({ 19 | url: string().url().required(), 20 | jobs: array(object({ 21 | label: string().required(), 22 | path: string().required(), 23 | branch: string() 24 | })).required(), 25 | interval: number(), 26 | title: string(), 27 | authKey: string() 28 | }) 29 | 30 | export default class JenkinsBuildDuration extends Component { 31 | static defaultProps = { 32 | interval: 1000 * 60 * 5, 33 | title: 'Build Duration' 34 | } 35 | 36 | state = { 37 | loading: true, 38 | error: false 39 | } 40 | 41 | componentDidMount () { 42 | schema.validate(this.props) 43 | .then(() => this.fetchInformation()) 44 | .catch((err) => { 45 | console.error(`${err.name} @ ${this.constructor.name}`, err.errors) 46 | this.setState({ error: true, loading: false }) 47 | }) 48 | } 49 | 50 | componentWillUnmount () { 51 | clearTimeout(this.timeout) 52 | } 53 | 54 | formatTime (ms) { 55 | const s = ms / 1000 56 | 57 | if (s > 60) { 58 | const min = Math.floor(s / 60) 59 | let minSec = Math.round(s - (min * 60)) 60 | minSec = minSec.toString().length === 1 ? `0${minSec}` : minSec 61 | 62 | return <>{min}:{minSec} min 63 | } 64 | 65 | return <>{Math.round(s)} sec 66 | } 67 | 68 | async fetchInformation () { 69 | const { authKey, jobs, url } = this.props 70 | const opts = authKey ? { headers: basicAuthHeader(authKey) } : {} 71 | 72 | try { 73 | const builds = await Promise.all( 74 | jobs.map(async job => { 75 | const branch = job.branch ? `job/${job.branch}/` : '' 76 | const res = await fetch(`${url}/job/${job.path}/${branch}lastBuild/api/json`, opts) 77 | const json = await res.json() 78 | 79 | return { 80 | name: job.label, 81 | url: json.url, 82 | duration: json.duration 83 | } 84 | }) 85 | ) 86 | 87 | this.setState({ error: false, loading: false, builds }) 88 | } catch (error) { 89 | this.setState({ error: true, loading: false }) 90 | } finally { 91 | this.timeout = setTimeout(() => this.fetchInformation(), this.props.interval) 92 | } 93 | } 94 | 95 | render () { 96 | const { loading, error, builds } = this.state 97 | const { title } = this.props 98 | 99 | return ( 100 | 101 | 102 | 103 | {builds && builds.map(build => ( 104 | 105 | 106 | 115 | 116 | ))} 117 | 118 |
{build.name} 107 | 108 | { 109 | build.duration 110 | ? this.formatTime(build.duration) 111 | : 112 | } 113 | 114 |
119 |
120 | ) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /components/widgets/jenkins/job-health.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | import fetch from 'isomorphic-unfetch' 3 | import styled from 'styled-components' 4 | import { object, string, array, number } from 'yup' 5 | 6 | import Widget from '../../widget' 7 | import Link from '../../link' 8 | import Table, { Th, Td } from '../../table' 9 | import LoadingIndicator from '../../loading-indicator' 10 | import { basicAuthHeader } from '../../../lib/auth' 11 | 12 | const jenkinsKpiColor = ({ theme, value }) => { 13 | if (value < 70) return theme.palette.errorColor 14 | if (value >= 70 && value < 90) return theme.palette.warnColor 15 | return theme.palette.successColor 16 | } 17 | 18 | const Kpi = styled.span` 19 | color: ${jenkinsKpiColor}; 20 | font-weight: 700; 21 | font-size: 20px; 22 | ` 23 | 24 | const schema = object().shape({ 25 | url: string().url().required(), 26 | jobs: array(object({ 27 | label: string().required(), 28 | path: string().required(), 29 | branch: string() 30 | })).required(), 31 | interval: number(), 32 | title: string(), 33 | authKey: string() 34 | }) 35 | 36 | export default class JenkinsJobHealth extends Component { 37 | static defaultProps = { 38 | interval: 1000 * 60 * 5, 39 | title: 'Job Health' 40 | } 41 | 42 | state = { 43 | loading: true, 44 | error: false 45 | } 46 | 47 | componentDidMount () { 48 | schema.validate(this.props) 49 | .then(() => this.fetchInformation()) 50 | .catch((err) => { 51 | console.error(`${err.name} @ ${this.constructor.name}`, err.errors) 52 | this.setState({ error: true, loading: false }) 53 | }) 54 | } 55 | 56 | componentWillUnmount () { 57 | clearTimeout(this.timeout) 58 | } 59 | 60 | async fetchInformation () { 61 | const { authKey, jobs, url } = this.props 62 | const opts = authKey ? { headers: basicAuthHeader(authKey) } : {} 63 | 64 | try { 65 | const builds = await Promise.all( 66 | jobs.map(async job => { 67 | const branch = job.branch ? `job/${job.branch}/` : '' 68 | const res = await fetch(`${url}/job/${job.path}/${branch}api/json`, opts) 69 | const json = await res.json() 70 | 71 | return { 72 | name: job.label, 73 | url: json.url, 74 | health: json.healthReport 75 | } 76 | }) 77 | ) 78 | 79 | this.setState({ error: false, loading: false, builds }) 80 | } catch (error) { 81 | this.setState({ error: true, loading: false }) 82 | } finally { 83 | this.timeout = setTimeout(() => this.fetchInformation(), this.props.interval) 84 | } 85 | } 86 | 87 | renderHealth (build) { 88 | return build.map((b, index, array) => ( 89 | 90 | {b.score} 91 | {index < array.length - 1 && / } 92 | 93 | )) 94 | } 95 | 96 | render () { 97 | const { loading, error, builds } = this.state 98 | const { title } = this.props 99 | 100 | return ( 101 | 102 | 103 | 104 | {builds && builds.map(build => ( 105 | 106 | 107 | 114 | 115 | ))} 116 | 117 |
{build.name} 108 | { 109 | build.health 110 | ? this.renderHealth(build.health) 111 | : 112 | } 113 |
118 |
119 | ) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /components/widgets/jenkins/job-status.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | import fetch from 'isomorphic-unfetch' 3 | import styled from 'styled-components' 4 | import { object, string, array, number } from 'yup' 5 | import Widget from '../../widget' 6 | import Table, { Th, Td } from '../../table' 7 | import Badge from '../../badge' 8 | import LoadingIndicator from '../../loading-indicator' 9 | import { basicAuthHeader } from '../../../lib/auth' 10 | 11 | const jenkinsBadgeColor = ({ theme, status }) => { 12 | switch (status) { 13 | case 'FAILURE': 14 | return theme.palette.errorColor 15 | case 'UNSTABLE': 16 | return theme.palette.warnColor 17 | case 'SUCCESS': 18 | return theme.palette.successColor 19 | case 'ABORTED': 20 | case 'NOT_BUILT': 21 | return theme.palette.disabledColor 22 | default: // null = 'In Progress' 23 | return 'transparent' 24 | } 25 | } 26 | const JenkinsBadge = styled(Badge)` 27 | background-color: ${jenkinsBadgeColor}; 28 | ` 29 | 30 | const schema = object().shape({ 31 | url: string().url().required(), 32 | jobs: array(object({ 33 | label: string().required(), 34 | path: string().required(), 35 | branch: string() 36 | })).required(), 37 | interval: number(), 38 | title: string(), 39 | authKey: string() 40 | }) 41 | 42 | export default class JenkinsJobStatus extends Component { 43 | static defaultProps = { 44 | interval: 1000 * 60 * 5, 45 | title: 'Job Status' 46 | } 47 | 48 | state = { 49 | loading: true, 50 | error: false 51 | } 52 | 53 | componentDidMount () { 54 | schema.validate(this.props) 55 | .then(() => this.fetchInformation()) 56 | .catch((err) => { 57 | console.error(`${err.name} @ ${this.constructor.name}`, err.errors) 58 | this.setState({ error: true, loading: false }) 59 | }) 60 | } 61 | 62 | componentWillUnmount () { 63 | clearTimeout(this.timeout) 64 | } 65 | 66 | async fetchInformation () { 67 | const { authKey, jobs, url } = this.props 68 | const opts = authKey ? { headers: basicAuthHeader(authKey) } : {} 69 | 70 | try { 71 | const builds = await Promise.all( 72 | jobs.map(async job => { 73 | const branch = job.branch ? `job/${job.branch}/` : '' 74 | const res = await fetch(`${url}/job/${job.path}/${branch}lastBuild/api/json`, opts) 75 | const json = await res.json() 76 | 77 | return { 78 | name: job.label, 79 | url: json.url, 80 | result: json.result 81 | } 82 | }) 83 | ) 84 | 85 | this.setState({ error: false, loading: false, builds }) 86 | } catch (error) { 87 | this.setState({ error: true, loading: false }) 88 | } finally { 89 | this.timeout = setTimeout(() => this.fetchInformation(), this.props.interval) 90 | } 91 | } 92 | 93 | render () { 94 | const { loading, error, builds } = this.state 95 | const { title } = this.props 96 | 97 | return ( 98 | 99 | 100 | 101 | {builds && builds.map(build => ( 102 | 103 | 104 | 113 | 114 | ))} 115 | 116 |
{build.name} 105 | 106 | { 107 | build.result 108 | ? 109 | : 110 | } 111 | 112 |
117 |
118 | ) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /components/widgets/jira/issue-count.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | import fetch from 'isomorphic-unfetch' 3 | import { object, string, number } from 'yup' 4 | import Widget from '../../widget' 5 | import Counter from '../../counter' 6 | import { basicAuthHeader } from '../../../lib/auth' 7 | 8 | const schema = object().shape({ 9 | url: string().url().required(), 10 | query: string().required(), 11 | interval: number(), 12 | title: string(), 13 | authKey: string() 14 | }) 15 | 16 | export default class JiraIssueCount extends Component { 17 | static defaultProps = { 18 | interval: 1000 * 60 * 5, 19 | title: 'JIRA Issue Count' 20 | } 21 | 22 | state = { 23 | count: 0, 24 | error: false, 25 | loading: true 26 | } 27 | 28 | componentDidMount () { 29 | schema.validate(this.props) 30 | .then(() => this.fetchInformation()) 31 | .catch((err) => { 32 | console.error(`${err.name} @ ${this.constructor.name}`, err.errors) 33 | this.setState({ error: true, loading: false }) 34 | }) 35 | } 36 | 37 | componentWillUnmount () { 38 | clearTimeout(this.timeout) 39 | } 40 | 41 | async fetchInformation () { 42 | const { authKey, url, query } = this.props 43 | const opts = authKey ? { headers: basicAuthHeader(authKey) } : {} 44 | 45 | try { 46 | const res = await fetch(`${url}/rest/api/2/search?jql=${query}`, opts) 47 | const json = await res.json() 48 | 49 | this.setState({ count: json.total, error: false, loading: false }) 50 | } catch (error) { 51 | this.setState({ error: true, loading: false }) 52 | } finally { 53 | this.timeout = setTimeout(() => this.fetchInformation(), this.props.interval) 54 | } 55 | } 56 | 57 | render () { 58 | const { count, error, loading } = this.state 59 | const { title } = this.props 60 | return ( 61 | 62 | 63 | 64 | ) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /components/widgets/jira/sprint-days-remaining.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | import fetch from 'isomorphic-unfetch' 3 | import { object, string, number } from 'yup' 4 | import Widget from '../../widget' 5 | import Counter from '../../counter' 6 | import { basicAuthHeader } from '../../../lib/auth' 7 | 8 | const schema = object().shape({ 9 | url: string().url().required(), 10 | boardId: number().required(), 11 | interval: number(), 12 | title: string(), 13 | authKey: string() 14 | }) 15 | 16 | export default class JiraSprintDaysRemaining extends Component { 17 | static defaultProps = { 18 | interval: 1000 * 60 * 60, 19 | title: 'JIRA Sprint Days Remaining' 20 | } 21 | 22 | state = { 23 | days: 0, 24 | error: false, 25 | loading: true 26 | } 27 | 28 | componentDidMount () { 29 | schema.validate(this.props) 30 | .then(() => this.fetchInformation()) 31 | .catch((err) => { 32 | console.error(`${err.name} @ ${this.constructor.name}`, err.errors) 33 | this.setState({ error: true, loading: false }) 34 | }) 35 | } 36 | 37 | componentWillUnmount () { 38 | clearTimeout(this.timeout) 39 | } 40 | 41 | calculateDays (date) { 42 | const currentDate = new Date() 43 | const endDate = new Date(date) 44 | const timeDiff = endDate.getTime() - currentDate.getTime() 45 | const diffDays = Math.ceil(timeDiff / (1000 * 3600 * 24)) 46 | 47 | return diffDays < 0 ? 0 : diffDays 48 | } 49 | 50 | async fetchInformation () { 51 | const { authKey, boardId, url } = this.props 52 | const opts = authKey ? { headers: basicAuthHeader(authKey) } : {} 53 | 54 | try { 55 | const res = await fetch(`${url}/rest/agile/1.0/board/${boardId}/sprint?state=active`, opts) 56 | const json = await res.json() 57 | const days = this.calculateDays(json.values[0].endDate) 58 | 59 | this.setState({ days, error: false, loading: false }) 60 | } catch (error) { 61 | this.setState({ error: true, loading: false }) 62 | } finally { 63 | this.timeout = setTimeout(() => this.fetchInformation(), this.props.interval) 64 | } 65 | } 66 | 67 | render () { 68 | const { days, error, loading } = this.state 69 | const { title } = this.props 70 | return ( 71 | 72 | 73 | 74 | ) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /components/widgets/pagespeed-insights/score.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | import fetch from 'isomorphic-unfetch' 3 | import { object, string, number, boolean } from 'yup' 4 | import CircleProgress from '../../circle-progress' 5 | import Widget from '../../widget' 6 | 7 | const schema = object().shape({ 8 | url: string().url().required(), 9 | filterThirdPartyResources: boolean(), 10 | interval: number(), 11 | strategy: string(), 12 | title: string() 13 | }) 14 | 15 | export default class PageSpeedInsightsScore extends Component { 16 | static defaultProps = { 17 | filterThirdPartyResources: false, 18 | interval: 1000 * 60 * 60 * 12, 19 | strategy: 'desktop', 20 | title: 'PageSpeed Score' 21 | } 22 | 23 | state = { 24 | score: 0, 25 | loading: true, 26 | error: false 27 | } 28 | 29 | componentDidMount () { 30 | schema.validate(this.props) 31 | .then(() => this.fetchInformation()) 32 | .catch((err) => { 33 | console.error(`${err.name} @ ${this.constructor.name}`, err.errors) 34 | this.setState({ error: true, loading: false }) 35 | }) 36 | } 37 | 38 | componentWillUnmount () { 39 | clearTimeout(this.timeout) 40 | } 41 | 42 | async fetchInformation () { 43 | const { url, filterThirdPartyResources, strategy } = this.props 44 | 45 | const searchParams = [ 46 | `url=${url}`, 47 | `filter_third_party_resources=${filterThirdPartyResources}`, 48 | `strategy=${strategy}` 49 | ].join('&') 50 | 51 | try { 52 | const res = await fetch(`https://www.googleapis.com/pagespeedonline/v2/runPagespeed?${searchParams}`) 53 | const json = await res.json() 54 | 55 | this.setState({ error: false, loading: false, score: json.ruleGroups.SPEED.score }) 56 | } catch (error) { 57 | this.setState({ error: true, loading: false }) 58 | } finally { 59 | this.timeout = setTimeout(() => this.fetchInformation(), this.props.interval) 60 | } 61 | } 62 | 63 | render () { 64 | const { error, loading, score } = this.state 65 | const { title } = this.props 66 | return ( 67 | 68 | 69 | 70 | ) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /components/widgets/pagespeed-insights/stats.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | import fetch from 'isomorphic-unfetch' 3 | import { object, string, boolean, number } from 'yup' 4 | import Table, { Th, Td } from '../../table' 5 | import Widget from '../../widget' 6 | 7 | const schema = object().shape({ 8 | url: string().url().required(), 9 | filterThirdPartyResources: boolean(), 10 | interval: number(), 11 | strategy: string(), 12 | title: string() 13 | }) 14 | 15 | export default class PageSpeedInsightsStats extends Component { 16 | static defaultProps = { 17 | filterThirdPartyResources: false, 18 | interval: 1000 * 60 * 60 * 12, 19 | strategy: 'desktop', 20 | title: 'PageSpeed Stats' 21 | } 22 | 23 | state = { 24 | stats: {}, 25 | loading: true, 26 | error: false 27 | } 28 | 29 | componentDidMount () { 30 | schema.validate(this.props) 31 | .then(() => this.fetchInformation()) 32 | .catch((err) => { 33 | console.error(`${err.name} @ ${this.constructor.name}`, err.errors) 34 | this.setState({ error: true, loading: false }) 35 | }) 36 | } 37 | 38 | componentWillUnmount () { 39 | clearTimeout(this.timeout) 40 | } 41 | 42 | bytesToKilobytes (bytes) { 43 | return bytes > 0 ? (bytes / 1024).toFixed(1) : 0 44 | } 45 | 46 | async fetchInformation () { 47 | const { url, filterThirdPartyResources, strategy } = this.props 48 | 49 | const searchParams = [ 50 | `url=${url}`, 51 | `filter_third_party_resources=${filterThirdPartyResources}`, 52 | `strategy=${strategy}` 53 | ].join('&') 54 | 55 | try { 56 | const res = await fetch(`https://www.googleapis.com/pagespeedonline/v2/runPagespeed?${searchParams}`) 57 | const json = await res.json() 58 | 59 | const pageStats = json.pageStats 60 | const stats = { 61 | cssCount: pageStats.numberCssResources || 0, 62 | cssSize: this.bytesToKilobytes(pageStats.cssResponseBytes), 63 | htmlSize: this.bytesToKilobytes(pageStats.htmlResponseBytes), 64 | imageSize: this.bytesToKilobytes(pageStats.imageResponseBytes), 65 | javascriptCount: pageStats.numberJsResources || 0, 66 | javascriptSize: this.bytesToKilobytes(pageStats.javascriptResponseBytes), 67 | requestCount: pageStats.numberResources || 0, 68 | requestSize: this.bytesToKilobytes(pageStats.totalRequestBytes), 69 | otherSize: this.bytesToKilobytes(pageStats.otherResponseBytes) 70 | } 71 | 72 | this.setState({ error: false, loading: false, stats }) 73 | } catch (error) { 74 | this.setState({ error: true, loading: false }) 75 | } finally { 76 | this.timeout = setTimeout(() => this.fetchInformation(), this.props.interval) 77 | } 78 | } 79 | 80 | render () { 81 | const { error, loading, stats } = this.state 82 | const { title } = this.props 83 | return ( 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 |
Request{stats.requestSize} KB ({stats.requestCount})
JavaScript{stats.javascriptSize} KB ({stats.javascriptCount})
CSS{stats.cssSize} KB ({stats.cssCount})
HTML{stats.htmlSize} KB
Image{stats.imageSize} KB
Other{stats.otherSize} KB
118 |
119 | ) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /components/widgets/sonarqube/index.js: -------------------------------------------------------------------------------- 1 | import { Component } from 'react' 2 | import styled from 'styled-components' 3 | import fetch from 'isomorphic-unfetch' 4 | import { object, string, number } from 'yup' 5 | import Widget from '../../widget' 6 | import Table, { Th, Td } from '../../table' 7 | import Badge from '../../badge' 8 | import { basicAuthHeader } from '../../../lib/auth' 9 | 10 | const alertColor = ({ theme, children }) => { 11 | switch (children) { 12 | case 'ERROR': 13 | return theme.palette.errorColor 14 | case 'WARN': 15 | return theme.palette.warnColor 16 | default: // OK 17 | return theme.palette.successColor 18 | } 19 | } 20 | const Alert = styled.span` 21 | color: ${alertColor}; 22 | ` 23 | 24 | const sonarBadgeColor = ({ theme, children }) => { 25 | switch (children) { 26 | case 'A': 27 | return theme.palette.successColor 28 | case 'B': 29 | return theme.palette.successSecondaryColor 30 | case 'C': 31 | return theme.palette.warnColor 32 | case 'D': 33 | return theme.palette.warnSecondaryColor 34 | case 'E': 35 | return theme.palette.errorColor 36 | default: 37 | return 'transparent' 38 | } 39 | } 40 | const SonarBadge = styled(Badge)` 41 | background-color: ${sonarBadgeColor}; 42 | ` 43 | 44 | const schema = object().shape({ 45 | url: string().url().required(), 46 | componentKey: string().required(), 47 | interval: number(), 48 | title: string(), 49 | authKey: string() 50 | }) 51 | 52 | export default class SonarQube extends Component { 53 | static defaultProps = { 54 | interval: 1000 * 60 * 5, 55 | title: 'SonarQube' 56 | } 57 | 58 | state = { 59 | measures: [], 60 | loading: true, 61 | error: false 62 | } 63 | 64 | componentDidMount () { 65 | schema.validate(this.props) 66 | .then(() => this.fetchInformation()) 67 | .catch((err) => { 68 | console.error(`${err.name} @ ${this.constructor.name}`, err.errors) 69 | this.setState({ error: true, loading: false }) 70 | }) 71 | } 72 | 73 | componentWillUnmount () { 74 | clearTimeout(this.timeout) 75 | } 76 | 77 | async fetchInformation () { 78 | const { authKey, url, componentKey } = this.props 79 | const opts = authKey ? { headers: basicAuthHeader(authKey) } : {} 80 | 81 | // https://docs.sonarqube.org/display/SONAR/Metric+Definitions 82 | const metricKeys = [ 83 | 'alert_status', 'reliability_rating', 'bugs', 'security_rating', 84 | 'vulnerabilities', 'sqale_rating', 'code_smells', 'coverage', 85 | 'duplicated_lines_density' 86 | ].join(',') 87 | 88 | try { 89 | const res = await fetch(`${url}/api/measures/component?componentKey=${componentKey}&metricKeys=${metricKeys}`, opts) 90 | const json = await res.json() 91 | 92 | this.setState({ error: false, loading: false, measures: json.component.measures }) 93 | } catch (error) { 94 | this.setState({ error: true, loading: false }) 95 | } finally { 96 | this.timeout = setTimeout(() => this.fetchInformation(), this.props.interval) 97 | } 98 | } 99 | 100 | getMetricValue = (measures, metricKey) => { 101 | const result = measures.filter(measure => measure.metric === metricKey) 102 | return result.length ? result[0].value : '' 103 | } 104 | 105 | getRatingValue = (measures, metricKey) => { 106 | const value = this.getMetricValue(measures, metricKey) 107 | 108 | switch (value) { 109 | case '1.0': 110 | return 'A' 111 | case '2.0': 112 | return 'B' 113 | case '3.0': 114 | return 'C' 115 | case '4.0': 116 | return 'D' 117 | case '5.0': 118 | return 'E' 119 | } 120 | 121 | return '?' 122 | } 123 | 124 | render () { 125 | const { error, loading, measures } = this.state 126 | const { title } = this.props 127 | 128 | const alertStatus = this.getMetricValue(measures, 'alert_status') 129 | const reliabilityRating = this.getRatingValue(measures, 'reliability_rating') 130 | const bugs = this.getMetricValue(measures, 'bugs') 131 | const securityRating = this.getRatingValue(measures, 'security_rating') 132 | const vulnerabilities = this.getMetricValue(measures, 'vulnerabilities') 133 | const sqaleRating = this.getRatingValue(measures, 'sqale_rating') 134 | const codeSmells = this.getMetricValue(measures, 'code_smells') 135 | const coverage = this.getMetricValue(measures, 'coverage') 136 | const duplicatedLinesDensity = this.getMetricValue(measures, 'duplicated_lines_density') 137 | 138 | return ( 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 152 | 153 | 154 | 155 | 156 | 159 | 160 | 161 | 162 | 163 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 |
Quality Gate:{alertStatus}
Reliability: 150 | {reliabilityRating} ({bugs}) 151 |
Security: 157 | {securityRating} ({vulnerabilities}) 158 |
Maintainability: 164 | {sqaleRating} ({codeSmells}) 165 |
Coverage:{coverage}%
Duplications:{duplicatedLinesDensity}%
179 |
180 | ) 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /components/widgets/title/index.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const Title = styled.h1` 4 | flex: 1 1 100%; 5 | font-size: 4em; 6 | margin: 0; 7 | padding: 0; 8 | text-align: center; 9 | ` 10 | 11 | export default Title 12 | -------------------------------------------------------------------------------- /lib/auth.js: -------------------------------------------------------------------------------- 1 | import { Base64 } from 'js-base64' 2 | import auth from '../auth' 3 | 4 | export const basicAuthHeader = (key) => { 5 | const credentials = auth[key] 6 | 7 | if (credentials) { 8 | const credential = Base64.encode(`${credentials.username}:${credentials.password}`) 9 | return { Authorization: `Basic ${credential}` } 10 | } 11 | 12 | throw new ReferenceError(`No credentials found with key '${key}' in auth.js`) 13 | } 14 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const Dotenv = require('dotenv-webpack') 2 | 3 | module.exports = { 4 | webpack: (config) => { 5 | config.plugins.push( 6 | new Dotenv({ path: './.env' }) 7 | ) 8 | return config 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dashboard", 3 | "version": "1.0.0", 4 | "description": "Your Team Dashboard", 5 | "main": "pages/index.js", 6 | "private": true, 7 | "scripts": { 8 | "dev": "next", 9 | "build": "next build", 10 | "start": "next start", 11 | "lint": "standard --verbose | snazzy && stylelint '**/*.js'", 12 | "test": "npm run lint" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "git+https://github.com/danielbayerlein/dashboard.git" 17 | }, 18 | "keywords": [ 19 | "tv", 20 | "dashboard" 21 | ], 22 | "author": "Daniel Bayerlein", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/danielbayerlein/dashboard/issues" 26 | }, 27 | "homepage": "https://github.com/danielbayerlein/dashboard#readme", 28 | "dependencies": { 29 | "babel-plugin-styled-components": "^1.13.3", 30 | "dotenv-webpack": "^2.0.0", 31 | "isomorphic-unfetch": "^3.1.0", 32 | "js-base64": "^3.7.2", 33 | "next": "^9.2.2", 34 | "polished": "^3.6.7", 35 | "react": "^16.13.1", 36 | "react-dom": "^16.13.1", 37 | "styled-components": "^4.4.1", 38 | "tinytime": "^0.2.6", 39 | "yup": "^0.32.11" 40 | }, 41 | "devDependencies": { 42 | "babel-eslint": "^10.1.0", 43 | "snazzy": "^8.0.0", 44 | "standard": "^16.0.4", 45 | "stylelint": "^13.13.1", 46 | "stylelint-config-standard": "^19.0.0", 47 | "stylelint-config-styled-components": "^0.1.1", 48 | "stylelint-processor-styled-components": "^1.10.0" 49 | }, 50 | "standard": { 51 | "parser": "babel-eslint" 52 | }, 53 | "babel": { 54 | "presets": [ 55 | "next/babel" 56 | ], 57 | "plugins": [ 58 | [ 59 | "styled-components", 60 | { 61 | "ssr": true, 62 | "displayName": true, 63 | "preprocess": false 64 | } 65 | ] 66 | ] 67 | }, 68 | "stylelint": { 69 | "processors": [ 70 | "stylelint-processor-styled-components" 71 | ], 72 | "extends": [ 73 | "stylelint-config-standard", 74 | "stylelint-config-styled-components" 75 | ], 76 | "syntax": "scss" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /pages/_document.js: -------------------------------------------------------------------------------- 1 | import Document from 'next/document' 2 | import { ServerStyleSheet } from 'styled-components' 3 | 4 | export default class MyDocument extends Document { 5 | static async getInitialProps (ctx) { 6 | const sheet = new ServerStyleSheet() 7 | const originalRenderPage = ctx.renderPage 8 | 9 | try { 10 | ctx.renderPage = () => 11 | originalRenderPage({ 12 | enhanceApp: App => props => sheet.collectStyles() 13 | }) 14 | 15 | const initialProps = await Document.getInitialProps(ctx) 16 | return { 17 | ...initialProps, 18 | styles: <>{initialProps.styles}{sheet.getStyleElement()} 19 | } 20 | } finally { 21 | sheet.seal() 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import Dashboard from '../components/dashboard' 2 | 3 | // Widgets 4 | import DateTime from '../components/widgets/datetime' 5 | import PageSpeedInsightsScore from '../components/widgets/pagespeed-insights/score' 6 | import PageSpeedInsightsStats from '../components/widgets/pagespeed-insights/stats' 7 | import JiraIssueCount from '../components/widgets/jira/issue-count' 8 | import SonarQube from '../components/widgets/sonarqube' 9 | import JenkinsJobStatus from '../components/widgets/jenkins/job-status' 10 | import JenkinsJobHealth from '../components/widgets/jenkins/job-health' 11 | import JenkinsBuildDuration from '../components/widgets/jenkins/build-duration' 12 | import BitbucketPullRequestCount from '../components/widgets/bitbucket/pull-request-count' 13 | import ElasticsearchHitCount from '../components/widgets/elasticsearch/hit-count' 14 | import GitHubIssueCount from '../components/widgets/github/issue-count' 15 | 16 | // Theme 17 | import lightTheme from '../styles/light-theme' 18 | // import darkTheme from '../styles/dark-theme' 19 | 20 | export default () => ( 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 33 | 34 | 40 | 41 | 45 | 46 | 53 | 54 | 61 | 62 | 69 | 70 | 76 | 77 | 81 | 82 | ) 83 | -------------------------------------------------------------------------------- /public/static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/danielbayerlein/dashboard/9935e150bdfd8525635a07421ec235da8452dba9/public/static/favicon.png -------------------------------------------------------------------------------- /styles/dark-theme.js: -------------------------------------------------------------------------------- 1 | const colors = { 2 | grey400: '#bdbdbd', 3 | grey700: '#616161', 4 | grey800: '#424242', 5 | grey: '#303030', 6 | white: '#ffffff', 7 | cyan500: '#00bcd4', 8 | pinkA200: '#ff4081', 9 | red500: '#f44336', 10 | amber500: '#ffc107', 11 | green500: '#4caf50', 12 | orange500: '#ff9800', 13 | lime500: '#cddc39' 14 | } 15 | 16 | export default { 17 | palette: { 18 | backgroundColor: colors.grey, 19 | borderColor: colors.grey700, 20 | textColor: colors.white, 21 | textInvertColor: colors.grey, 22 | canvasColor: colors.grey800, 23 | primaryColor: colors.cyan500, 24 | accentColor: colors.pinkA200, 25 | errorColor: colors.red500, 26 | warnColor: colors.amber500, 27 | warnSecondaryColor: colors.orange500, 28 | successColor: colors.green500, 29 | successSecondaryColor: colors.lime500, 30 | disabledColor: colors.grey400 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /styles/light-theme.js: -------------------------------------------------------------------------------- 1 | const colors = { 2 | grey50: '#fafafa', 3 | grey200: '#eeeeee', 4 | grey400: '#bdbdbd', 5 | grey900: '#212121', 6 | white: '#ffffff', 7 | cyan500: '#00bcd4', 8 | pinkA200: '#ff4081', 9 | redA700: '#d50000', 10 | amberA700: '#ffab00', 11 | greenA700: '#00c853', 12 | lightGreenA700: '#64dd17', 13 | orangeA700: '#ff6d00' 14 | } 15 | 16 | export default { 17 | palette: { 18 | backgroundColor: colors.grey50, 19 | borderColor: colors.grey200, 20 | textColor: colors.grey900, 21 | textInvertColor: colors.grey50, 22 | canvasColor: colors.white, 23 | primaryColor: colors.cyan500, 24 | accentColor: colors.pinkA200, 25 | errorColor: colors.redA700, 26 | warnColor: colors.amberA700, 27 | warnSecondaryColor: colors.orangeA700, 28 | successColor: colors.greenA700, 29 | successSecondaryColor: colors.lightGreenA700, 30 | disabledColor: colors.grey400 31 | } 32 | } 33 | --------------------------------------------------------------------------------