diff --git a/components/widget.js b/components/widget.js index 0f23cef7..b894ebf3 100644 --- a/components/widget.js +++ b/components/widget.js @@ -2,12 +2,13 @@ import styled from 'styled-components' import { size } from 'polished' import LoadingIndicator from './loading-indicator' import ErrorIcon from './error-icon' +import { NONE } from '../lib/alert' const Container = styled.div` ${size('20em')} align-items: center; background-color: ${props => props.theme.palette.canvasColor}; - border: 1px solid ${props => props.theme.palette.borderColor}; + border: 1px solid ${props => props.theme.atoms.Widget[props.alertSeverity].border}; display: flex; flex-direction: column; justify-content: center; @@ -19,19 +20,19 @@ const Title = styled.h1` text-align: center; ` -export default ({ children, error = false, loading = false, title = '' }) => { +export default ({ children, hasError = false, isLoading = false, alertSeverity = NONE, title = '' }) => { let content - if (loading) { + if (isLoading) { content = - } else if (error) { + } else if (hasError) { content = } else { content =
{children}
} return ( - + {title ? {title} : ''} {content} diff --git a/components/widgets/bitbucket/pull-request-count.js b/components/widgets/bitbucket/pull-request-count.js index 97810186..017da924 100644 --- a/components/widgets/bitbucket/pull-request-count.js +++ b/components/widgets/bitbucket/pull-request-count.js @@ -1,9 +1,10 @@ import { Component } from 'react' import fetch from 'isomorphic-unfetch' -import { object, string, number, array } from 'yup' +import { array, object, string, number } from 'yup' import Widget from '../../widget' import Counter from '../../counter' import { basicAuthHeader } from '../../../lib/auth' +import { severity, NONE } from '../../../lib/alert' const schema = object().shape({ url: string().url().required(), @@ -12,7 +13,11 @@ const schema = object().shape({ interval: number(), title: string(), users: array().of(string()), - authKey: string() + authKey: string(), + alert: array(object({ + severity: string().required(), + value: number().required() + })) }) export default class BitbucketPullRequestCount extends Component { @@ -24,8 +29,9 @@ export default class BitbucketPullRequestCount extends Component { state = { count: 0, - error: false, - loading: true + hasError: false, + isLoading: true, + alertSeverity: NONE } componentDidMount () { @@ -33,7 +39,7 @@ export default class BitbucketPullRequestCount extends Component { .then(() => this.fetchInformation()) .catch((err) => { console.error(`${err.name} @ ${this.constructor.name}`, err.errors) - this.setState({ error: true, loading: false }) + this.setState({ hasError: true, isLoading: false }) }) } @@ -42,7 +48,7 @@ export default class BitbucketPullRequestCount extends Component { } async fetchInformation () { - const { authKey, url, project, repository, users } = this.props + const { authKey, url, project, repository, users, alert } = this.props const opts = authKey ? { headers: basicAuthHeader(authKey) } : {} try { @@ -56,19 +62,24 @@ export default class BitbucketPullRequestCount extends Component { count = json.size } - this.setState({ count, error: false, loading: false }) - } catch (error) { - this.setState({ error: true, loading: false }) + this.setState({ + count, + hasError: false, + isLoading: false, + alertSeverity: severity(count, alert) + }) + } catch (err) { + this.setState({ hasError: true, isLoading: false, alertSeverity: NONE }) } finally { this.timeout = setTimeout(() => this.fetchInformation(), this.props.interval) } } render () { - const { count, error, loading } = this.state + const { count, hasError, isLoading, alertSeverity } = this.state const { title } = this.props return ( - + ) diff --git a/components/widgets/elasticsearch/hit-count.js b/components/widgets/elasticsearch/hit-count.js index dbce320d..c18116f6 100644 --- a/components/widgets/elasticsearch/hit-count.js +++ b/components/widgets/elasticsearch/hit-count.js @@ -1,16 +1,21 @@ import { Component } from 'react' import fetch from 'isomorphic-unfetch' -import { object, string, number } from 'yup' +import { array, object, string, number } from 'yup' import Widget from '../../widget' import Counter from '../../counter' import { basicAuthHeader } from '../../../lib/auth' +import { severity, NONE } from '../../../lib/alert' const schema = object().shape({ url: string().url().required(), index: string().required(), query: string().required(), interval: number(), - title: string() + title: string(), + alert: array(object({ + severity: string().required(), + value: number().required() + })) }) export default class ElasticsearchHitCount extends Component { @@ -21,8 +26,9 @@ export default class ElasticsearchHitCount extends Component { state = { count: 0, - error: false, - loading: true + hasError: false, + isLoading: true, + alertSeverity: NONE } componentDidMount () { @@ -30,7 +36,7 @@ export default class ElasticsearchHitCount extends Component { .then(() => this.fetchInformation()) .catch((err) => { console.error(`${err.name} @ ${this.constructor.name}`, err.errors) - this.setState({ error: true, loading: false }) + this.setState({ hasError: true, isLoading: false }) }) } @@ -39,26 +45,33 @@ export default class ElasticsearchHitCount extends Component { } async fetchInformation () { - const { authKey, index, query, url } = this.props + const { authKey, index, query, url, alert } = this.props const opts = authKey ? { headers: basicAuthHeader(authKey) } : {} try { const res = await fetch(`${url}/${index}/_search?q=${query}`, opts) const json = await res.json() + const total = json.hits.total - this.setState({ count: json.hits.total, error: false, loading: false }) - } catch (error) { - this.setState({ error: true, loading: false }) + this.setState({ + count: total, + hasError: false, + isLoading: false, + alertSeverity: severity(total, alert) + }) + } catch (err) { + this.setState({ hasError: true, isLoading: false, alertSeverity: NONE }) } finally { this.timeout = setTimeout(() => this.fetchInformation(), this.props.interval) } } render () { - const { count, error, loading } = this.state + const { count, hasError, isLoading, alertSeverity } = this.state const { title } = this.props + return ( - + ) diff --git a/components/widgets/github/issue-count.js b/components/widgets/github/issue-count.js index 3594ddad..0199456b 100644 --- a/components/widgets/github/issue-count.js +++ b/components/widgets/github/issue-count.js @@ -1,16 +1,21 @@ import { Component } from 'react' import fetch from 'isomorphic-unfetch' -import { object, string, number } from 'yup' +import { array, object, string, number } from 'yup' import Widget from '../../widget' import Counter from '../../counter' import { basicAuthHeader } from '../../../lib/auth' +import { severity, NONE } from '../../../lib/alert' const schema = object().shape({ owner: string().required(), repository: string().required(), interval: number(), title: string(), - authKey: string() + authKey: string(), + alert: array(object({ + severity: string().required(), + value: number().required() + })) }) export default class GitHubIssueCount extends Component { @@ -21,8 +26,9 @@ export default class GitHubIssueCount extends Component { state = { count: 0, - error: false, - loading: true + hasError: false, + isLoading: true, + alertSeverity: NONE } componentDidMount () { @@ -30,7 +36,7 @@ export default class GitHubIssueCount extends Component { .then(() => this.fetchInformation()) .catch((err) => { console.error(`${err.name} @ ${this.constructor.name}`, err.errors) - this.setState({ error: true, loading: false }) + this.setState({ hasError: true, isLoading: false }) }) } @@ -39,26 +45,32 @@ export default class GitHubIssueCount extends Component { } async fetchInformation () { - const { authKey, owner, repository } = this.props + const { authKey, owner, repository, alert } = this.props const opts = authKey ? { headers: basicAuthHeader(authKey) } : {} try { const res = await fetch(`https://api.github.com/repos/${owner}/${repository}`, opts) const json = await res.json() + const total = json.open_issues_count - this.setState({ count: json.open_issues_count, error: false, loading: false }) - } catch (error) { - this.setState({ error: true, loading: false }) + this.setState({ + count: total, + hasError: false, + isLoading: false, + alertSeverity: severity(total, alert) + }) + } catch (err) { + this.setState({ hasError: true, isLoading: false, alertSeverity: NONE }) } finally { this.timeout = setTimeout(() => this.fetchInformation(), this.props.interval) } } render () { - const { count, error, loading } = this.state + const { count, hasError, isLoading, alertSeverity } = this.state const { title } = this.props return ( - + ) diff --git a/components/widgets/jenkins/job-status.js b/components/widgets/jenkins/job-status.js index 03f0d04c..729306d8 100644 --- a/components/widgets/jenkins/job-status.js +++ b/components/widgets/jenkins/job-status.js @@ -46,8 +46,8 @@ export default class JenkinsJobStatus extends Component { } state = { - loading: true, - error: false + isLoading: true, + hasError: false } componentDidMount () { @@ -55,7 +55,7 @@ export default class JenkinsJobStatus extends Component { .then(() => this.fetchInformation()) .catch((err) => { console.error(`${err.name} @ ${this.constructor.name}`, err.errors) - this.setState({ error: true, loading: false }) + this.setState({ hasError: true, isLoading: false }) }) } @@ -82,20 +82,20 @@ export default class JenkinsJobStatus extends Component { }) ) - this.setState({ error: false, loading: false, builds }) - } catch (error) { - this.setState({ error: true, loading: false }) + this.setState({ hasError: false, isLoading: false, builds }) + } catch (err) { + this.setState({ hasError: true, isLoading: false }) } finally { this.timeout = setTimeout(() => this.fetchInformation(), this.props.interval) } } render () { - const { loading, error, builds } = this.state + const { isLoading, hasError, builds } = this.state const { title } = this.props return ( - + {builds && builds.map(build => ( diff --git a/components/widgets/jira/issue-count.js b/components/widgets/jira/issue-count.js index e4335528..acaabeb0 100644 --- a/components/widgets/jira/issue-count.js +++ b/components/widgets/jira/issue-count.js @@ -1,16 +1,21 @@ import { Component } from 'react' import fetch from 'isomorphic-unfetch' -import { object, string, number } from 'yup' +import { array, object, string, number } from 'yup' import Widget from '../../widget' import Counter from '../../counter' import { basicAuthHeader } from '../../../lib/auth' +import { severity, NONE } from '../../../lib/alert' const schema = object().shape({ url: string().url().required(), query: string().required(), interval: number(), title: string(), - authKey: string() + authKey: string(), + alert: array(object({ + severity: string().required(), + value: number().required() + })) }) export default class JiraIssueCount extends Component { @@ -21,8 +26,9 @@ export default class JiraIssueCount extends Component { state = { count: 0, - error: false, - loading: true + hasError: false, + isLoading: true, + alertSeverity: NONE } componentDidMount () { @@ -30,7 +36,7 @@ export default class JiraIssueCount extends Component { .then(() => this.fetchInformation()) .catch((err) => { console.error(`${err.name} @ ${this.constructor.name}`, err.errors) - this.setState({ error: true, loading: false }) + this.setState({ hasError: true, isLoading: false }) }) } @@ -39,26 +45,33 @@ export default class JiraIssueCount extends Component { } async fetchInformation () { - const { authKey, url, query } = this.props + const { authKey, url, query, alert } = this.props const opts = authKey ? { headers: basicAuthHeader(authKey) } : {} try { const res = await fetch(`${url}/rest/api/2/search?jql=${query}`, opts) const json = await res.json() + const total = json.total - this.setState({ count: json.total, error: false, loading: false }) - } catch (error) { - this.setState({ error: true, loading: false }) + this.setState({ + count: total, + hasError: false, + isLoading: false, + alertSeverity: severity(total, alert) + }) + } catch (err) { + this.setState({ hasError: true, isLoading: false, alertSeverity: NONE }) } finally { this.timeout = setTimeout(() => this.fetchInformation(), this.props.interval) } } render () { - const { count, error, loading } = this.state + const { count, hasError, isLoading, alertSeverity } = this.state const { title } = this.props + return ( - + ) diff --git a/components/widgets/jira/sprint-days-remaining.js b/components/widgets/jira/sprint-days-remaining.js index a0feb217..7f34c5c4 100644 --- a/components/widgets/jira/sprint-days-remaining.js +++ b/components/widgets/jira/sprint-days-remaining.js @@ -21,8 +21,8 @@ export default class JiraSprintDaysRemaining extends Component { state = { days: 0, - error: false, - loading: true + hasError: false, + isLoading: true } componentDidMount () { @@ -30,7 +30,7 @@ export default class JiraSprintDaysRemaining extends Component { .then(() => this.fetchInformation()) .catch((err) => { console.error(`${err.name} @ ${this.constructor.name}`, err.errors) - this.setState({ error: true, loading: false }) + this.setState({ hasError: true, isLoading: false }) }) } @@ -56,19 +56,19 @@ export default class JiraSprintDaysRemaining extends Component { const json = await res.json() const days = this.calculateDays(json.values[0].endDate) - this.setState({ days, error: false, loading: false }) - } catch (error) { - this.setState({ error: true, loading: false }) + this.setState({ days, hasError: false, isLoading: false }) + } catch (err) { + this.setState({ hasError: true, isLoading: false }) } finally { this.timeout = setTimeout(() => this.fetchInformation(), this.props.interval) } } render () { - const { days, error, loading } = this.state + const { days, hasError, isLoading } = this.state const { title } = this.props return ( - + ) diff --git a/components/widgets/pagespeed-insights/score.js b/components/widgets/pagespeed-insights/score.js index 47a8ca6d..a028680f 100644 --- a/components/widgets/pagespeed-insights/score.js +++ b/components/widgets/pagespeed-insights/score.js @@ -1,15 +1,20 @@ import { Component } from 'react' import fetch from 'isomorphic-unfetch' -import { object, string, number, boolean } from 'yup' +import { array, object, string, number, boolean } from 'yup' import CircleProgress from '../../circle-progress' import Widget from '../../widget' +import { severity, NONE } from '../../../lib/alert' const schema = object().shape({ url: string().url().required(), filterThirdPartyResources: boolean(), interval: number(), strategy: string(), - title: string() + title: string(), + alert: array(object({ + severity: string().required(), + value: number().required() + })) }) export default class PageSpeedInsightsScore extends Component { @@ -22,8 +27,9 @@ export default class PageSpeedInsightsScore extends Component { state = { score: 0, - loading: true, - error: false + isLoading: true, + hasError: false, + alertSeverity: NONE } componentDidMount () { @@ -31,7 +37,7 @@ export default class PageSpeedInsightsScore extends Component { .then(() => this.fetchInformation()) .catch((err) => { console.error(`${err.name} @ ${this.constructor.name}`, err.errors) - this.setState({ error: true, loading: false }) + this.setState({ hasError: true, isLoading: false }) }) } @@ -40,7 +46,7 @@ export default class PageSpeedInsightsScore extends Component { } async fetchInformation () { - const { url, filterThirdPartyResources, strategy } = this.props + const { url, filterThirdPartyResources, strategy, alert } = this.props const searchParams = [ `url=${url}`, @@ -51,20 +57,27 @@ export default class PageSpeedInsightsScore extends Component { try { const res = await fetch(`https://www.googleapis.com/pagespeedonline/v2/runPagespeed?${searchParams}`) const json = await res.json() + const score = json.ruleGroups.SPEED.score - this.setState({ error: false, loading: false, score: json.ruleGroups.SPEED.score }) - } catch (error) { - this.setState({ error: true, loading: false }) + this.setState({ + score, + hasError: false, + isLoading: false, + alertSeverity: severity(score, alert) + }) + } catch (err) { + this.setState({ hasError: true, isLoading: false, alertSeverity: NONE }) } finally { this.timeout = setTimeout(() => this.fetchInformation(), this.props.interval) } } render () { - const { error, loading, score } = this.state + const { hasError, isLoading, score, alertSeverity } = this.state const { title } = this.props + return ( - + ) diff --git a/components/widgets/pagespeed-insights/stats.js b/components/widgets/pagespeed-insights/stats.js index e92813d0..40f53df5 100644 --- a/components/widgets/pagespeed-insights/stats.js +++ b/components/widgets/pagespeed-insights/stats.js @@ -22,8 +22,8 @@ export default class PageSpeedInsightsStats extends Component { state = { stats: {}, - loading: true, - error: false + isLoading: true, + hasError: false } componentDidMount () { @@ -31,7 +31,7 @@ export default class PageSpeedInsightsStats extends Component { .then(() => this.fetchInformation()) .catch((err) => { console.error(`${err.name} @ ${this.constructor.name}`, err.errors) - this.setState({ error: true, loading: false }) + this.setState({ hasError: true, isLoading: false }) }) } @@ -69,19 +69,19 @@ export default class PageSpeedInsightsStats extends Component { otherSize: this.bytesToKilobytes(pageStats.otherResponseBytes) } - this.setState({ error: false, loading: false, stats }) - } catch (error) { - this.setState({ error: true, loading: false }) + this.setState({ hasError: false, isLoading: false, stats }) + } catch (err) { + this.setState({ hasError: true, isLoading: false }) } finally { this.timeout = setTimeout(() => this.fetchInformation(), this.props.interval) } } render () { - const { error, loading, stats } = this.state + const { hasError, isLoading, stats } = this.state const { title } = this.props return ( - +
diff --git a/components/widgets/sonarqube/index.js b/components/widgets/sonarqube/index.js index 73f11cda..9789542a 100644 --- a/components/widgets/sonarqube/index.js +++ b/components/widgets/sonarqube/index.js @@ -57,8 +57,8 @@ export default class SonarQube extends Component { state = { measures: [], - loading: true, - error: false + isLoading: true, + hasError: false } componentDidMount () { @@ -66,7 +66,7 @@ export default class SonarQube extends Component { .then(() => this.fetchInformation()) .catch((err) => { console.error(`${err.name} @ ${this.constructor.name}`, err.errors) - this.setState({ error: true, loading: false }) + this.setState({ hasError: true, isLoading: false }) }) } @@ -89,9 +89,9 @@ export default class SonarQube extends Component { const res = await fetch(`${url}/api/measures/component?componentKey=${componentKey}&metricKeys=${metricKeys}`, opts) const json = await res.json() - this.setState({ error: false, loading: false, measures: json.component.measures }) - } catch (error) { - this.setState({ error: true, loading: false }) + this.setState({ hasError: false, isLoading: false, measures: json.component.measures }) + } catch (err) { + this.setState({ hasError: true, isLoading: false }) } finally { this.timeout = setTimeout(() => this.fetchInformation(), this.props.interval) } @@ -122,7 +122,7 @@ export default class SonarQube extends Component { } render () { - const { error, loading, measures } = this.state + const { hasError, isLoading, measures } = this.state const { title } = this.props const alertStatus = this.getMetricValue(measures, 'alert_status') @@ -136,7 +136,7 @@ export default class SonarQube extends Component { const duplicatedLinesDensity = this.getMetricValue(measures, 'duplicated_lines_density') return ( - +
diff --git a/lib/alert.js b/lib/alert.js new file mode 100644 index 00000000..b4fcc908 --- /dev/null +++ b/lib/alert.js @@ -0,0 +1,28 @@ +export const NONE = 'none' +export const WARNING = 'warning' +export const CRITICAL = 'critical' + +export const severity = (value, alert) => ( + Number.isInteger(value) + ? severityAsInteger(value, alert) + : severityAsString(value, alert) +) + +const severityAsInteger = (value, alert) => { + const alertSeverityCritical = alert.find(item => item.severity === CRITICAL) + if (alertSeverityCritical && value >= alertSeverityCritical.value) { + return CRITICAL + } + + const alertSeverityWarning = alert.find(item => item.severity === WARNING) + if (alertSeverityWarning && value >= alertSeverityWarning.value) { + return WARNING + } + + return NONE +} + +const severityAsString = (value, alert) => { + const alertItem = alert.find(item => item.value === value) + return alertItem ? alertItem.severity : NONE +} diff --git a/pages/index.js b/pages/index.js index c8637a16..6e69a281 100644 --- a/pages/index.js +++ b/pages/index.js @@ -29,6 +29,10 @@ export default () => ( title='JIRA Open Bugs' url='https://crossorigin.me/https://jira.atlassian.com' query='type=Bug AND project="Bitbucket Server" AND resolution=Unresolved ORDER BY priority DESC,created DESC' + alert={[ + { severity: 'warning', value: 5 }, + { severity: 'critical', value: 10 } + ]} />