├── .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 |
3 |
4 |
5 |
6 | Dashboard
7 |
8 |
9 |
10 | Create your own team dashboard with custom widgets.
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
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 | 
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 | 
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 |
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 |
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 |
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 | {build.name} |
106 |
107 |
108 | {
109 | build.duration
110 | ? this.formatTime(build.duration)
111 | :
112 | }
113 |
114 | |
115 |
116 | ))}
117 |
118 |
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 | {build.name} |
107 |
108 | {
109 | build.health
110 | ? this.renderHealth(build.health)
111 | :
112 | }
113 | |
114 |
115 | ))}
116 |
117 |
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 |
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 | Request |
89 | {stats.requestSize} KB ({stats.requestCount}) |
90 |
91 |
92 |
93 | JavaScript |
94 | {stats.javascriptSize} KB ({stats.javascriptCount}) |
95 |
96 |
97 |
98 | CSS |
99 | {stats.cssSize} KB ({stats.cssCount}) |
100 |
101 |
102 |
103 | HTML |
104 | {stats.htmlSize} KB |
105 |
106 |
107 |
108 | Image |
109 | {stats.imageSize} KB |
110 |
111 |
112 |
113 | Other |
114 | {stats.otherSize} KB |
115 |
116 |
117 |
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 | Quality Gate: |
144 | {alertStatus} |
145 |
146 |
147 |
148 | Reliability: |
149 |
150 | {reliabilityRating} ({bugs})
151 | |
152 |
153 |
154 |
155 | Security: |
156 |
157 | {securityRating} ({vulnerabilities})
158 | |
159 |
160 |
161 |
162 | Maintainability: |
163 |
164 | {sqaleRating} ({codeSmells})
165 | |
166 |
167 |
168 |
169 | Coverage: |
170 | {coverage}% |
171 |
172 |
173 |
174 | Duplications: |
175 | {duplicatedLinesDensity}% |
176 |
177 |
178 |
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 |
--------------------------------------------------------------------------------