diff --git a/src/client.js b/src/client.js index 626a5c3..c12018b 100644 --- a/src/client.js +++ b/src/client.js @@ -64,7 +64,35 @@ const client = mozaik => { }; }) ; - } + }, + projectsMergeRequests({ projects }) { + const reqs = projects.map((project) => { + return buildApiRequest(`/projects/${encodeURIComponent(project)}/merge_requests`); + }); + return Promise.props({ + mergeRequests: Promise.all(reqs).then((data) => { + return data.map((item) => item.body); + }) + }); + }, + groupMergeRequests({ groups, query={} }) { + const reqs = groups.map((group) => { + return buildApiRequest(`/groups/${encodeURIComponent(group)}/merge_requests`, query); + }); + return Promise.props({ + mergeRequests: Promise.all(reqs).then((data) => { + return data.map((item) => item.body); + }) + }); + }, + projectPipelines({ project, query={} }) { + return Promise.props({ + project: operations.project({ project }), + pipelines: buildApiRequest(`/projects/${encodeURIComponent(project)}/pipelines`, query).then((res) => { + return res.body; + }) + }); + }, }; return operations; diff --git a/src/components/MergeRequest.jsx b/src/components/MergeRequest.jsx new file mode 100644 index 0000000..30e7bdb --- /dev/null +++ b/src/components/MergeRequest.jsx @@ -0,0 +1,55 @@ +import React, { Component, PropTypes } from 'react'; +import moment from 'moment'; + + +class MergeRequestItem extends Component { + render() { + const { mergeRequest } = this.props; + + const cssClasses = `list__item`; + + return ( +
+ + #{mergeRequest.title} + +
+ + + by + {mergeRequest.author.name} + + + upvotes + {mergeRequest.upvotes} + +   +
+ + +
+ ); + } +} + +MergeRequestItem.displayName = 'MergeRequestItem'; + +MergeRequestItem.propTypes = { + project: PropTypes.shape({ + web_url: PropTypes.string.isRequired + }).isRequired, + build: PropTypes.shape({ + id: PropTypes.number.isRequired, + status: PropTypes.string.isRequired, + finished_at: PropTypes.string, + commit: PropTypes.shape({ + message: PropTypes.string.isRequired + }) + }).isRequired +}; + + +export default MergeRequestItem; \ No newline at end of file diff --git a/src/components/MergeRequestGaugeGroups.jsx b/src/components/MergeRequestGaugeGroups.jsx new file mode 100644 index 0000000..ea647a2 --- /dev/null +++ b/src/components/MergeRequestGaugeGroups.jsx @@ -0,0 +1,105 @@ +import React, { Component, PropTypes } from 'react'; +import reactMixin from 'react-mixin'; +import { ListenerMixin } from 'reflux'; +import _ from 'lodash'; +import Mozaik from 'mozaik/browser'; +const { Gauge } = Mozaik.Component; + + +class MergeRequestsGaugeGroups extends Component { + constructor(props) { + super(props); + + this.state = { mergeRequestCount: 0 }; + } + + getApiRequest() { + const { groups } = this.props; + + return { + id: `gitlab.groupMergeRequests.${ groups }`, + params: { + groups, + query: { + state: 'opened' + } + } + }; + } + + onApiData({ mergeRequests }) { + const flattenArray = (arr) => [].concat.apply([], arr); + this.setState({ mergeRequestCount: flattenArray(mergeRequests).length }); + } + + render() { + const { thresholds } = this.props; + const { mergeRequestCount } = this.state; + + const cappedValue = Math.min(mergeRequestCount, _.max(thresholds.map(threshold => threshold.threshold))); + let message = null; + const normThresholds = thresholds.map(threshold => { + if (message === null && cappedValue <= threshold.threshold) { + message = threshold.message; + } + + return { + upperBound: threshold.threshold, + color: threshold.color + }; + }); + + return ( +
+
+ Pending Merge Requests + + {mergeRequestCount} + + +
+
+
+ +
+
+ {message} +
+
+
+ ); + } +} + +MergeRequestsGaugeGroups.displayName = 'MergeRequestsGaugeGroups'; + +MergeRequestsGaugeGroups.propTypes = { + project: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number + ]).isRequired, + thresholds: PropTypes.arrayOf(PropTypes.shape({ + threshold: PropTypes.number.isRequired, + color: PropTypes.string.isRequired, + message: PropTypes.string.isRequired + })).isRequired +}; + +MergeRequestsGaugeGroups.defaultProps = { + thresholds: [ + { threshold: 3, color: '#85e985', message: 'good job!' }, + { threshold: 5, color: '#ecc265', message: 'you should consider reviewing' }, + { threshold: 10, color: '#f26a3f', message: 'merge requests overflow' } + ] +}; + +reactMixin(MergeRequestsGaugeGroups.prototype, ListenerMixin); +reactMixin(MergeRequestsGaugeGroups.prototype, Mozaik.Mixin.ApiConsumer); + + +export default MergeRequestsGaugeGroups; \ No newline at end of file diff --git a/src/components/MergeRequests.jsx b/src/components/MergeRequests.jsx new file mode 100644 index 0000000..3903f3d --- /dev/null +++ b/src/components/MergeRequests.jsx @@ -0,0 +1,64 @@ +import React, { Component, PropTypes } from 'react'; +import reactMixin from 'react-mixin'; +import { ListenerMixin } from 'reflux'; +import Mozaik from 'mozaik/browser'; +import MergeRequestItem from './MergeRequest'; + + +class MergeRequests extends Component { + constructor(props) { + super(props); + + this.state = { + mergeRequests: [], + groups: [], + query: {} + }; + } + + getApiRequest() { + const { groups, query } = this.props; + + return { + id: `gitlab.groupMergeRequests.${ groups }`, + params: { groups, query } + }; + } + + onApiData({ groups, mergeRequests }) { + const flattenArray = (arr) => [].concat.apply([], arr); + this.setState({ groups, mergeRequests: flattenArray(mergeRequests) }); + } + + render() { + const { groups, mergeRequests } = this.state; + + return ( +
+
+ MergeRequests + + {mergeRequests.length} + + +
+
+ {mergeRequests.map(mr => ())} +
+
+ ); + } +} + +MergeRequests.propTypes = { + groups: PropTypes.oneOfType([ + PropTypes.string, + PropTypes.number + ]).isRequired +}; + +reactMixin(MergeRequests.prototype, ListenerMixin); +reactMixin(MergeRequests.prototype, Mozaik.Mixin.ApiConsumer); + + +export default MergeRequests; \ No newline at end of file diff --git a/src/components/PipelinesHistory.jsx b/src/components/PipelinesHistory.jsx new file mode 100644 index 0000000..52ed78d --- /dev/null +++ b/src/components/PipelinesHistory.jsx @@ -0,0 +1,59 @@ +import React, { Component, PropTypes } from 'react'; +import reactMixin from 'react-mixin'; +import { ListenerMixin } from 'reflux'; +import Mozaik from 'mozaik/browser'; +import PipelineHistoryItem from './PipelinesHistoryItem.jsx'; + +class PipelinesHistory extends Component { + constructor(props) { + super(props); + + this.state = { + project: null, + pipelines: [] + }; + } + + getApiRequest() { + const { project, query } = this.props; + + return { + id: `gitlab.projectPipelines.${project}`, + params: { project, query } + }; + } + + onApiData(data) { + const { project, pipelines } = data; + this.setState({ project, pipelines }); + } + + render() { + const { project, pipelines } = this.state; + + return ( +
+
+ pipelines history + +
+
+ {pipelines.map((pipeline) => ( + + ))} +
+
+ ); + } +} + +PipelinesHistory.displayName = 'PipelinesHistory'; + +PipelinesHistory.propTypes = { + project: PropTypes.oneOfType([ PropTypes.string, PropTypes.number ]).isRequired +}; + +reactMixin(PipelinesHistory.prototype, ListenerMixin); +reactMixin(PipelinesHistory.prototype, Mozaik.Mixin.ApiConsumer); + +export default PipelinesHistory; \ No newline at end of file diff --git a/src/components/PipelinesHistoryItem.jsx b/src/components/PipelinesHistoryItem.jsx new file mode 100644 index 0000000..a695748 --- /dev/null +++ b/src/components/PipelinesHistoryItem.jsx @@ -0,0 +1,36 @@ +import React, { Component, PropTypes } from 'react'; +class PipelineHistoryItem extends Component { + render() { + const { project, pipeline } = this.props; + + const cssClasses = `list__item list__item--with-status list__item--with-status--${pipeline.status}`; + + return ( +
+ + #{pipeline.id} + + + ref + {pipeline.ref} + +   +
+
+ ); + } +} + +PipelineHistoryItem.displayName = 'PipelineHistoryItem'; + +PipelineHistoryItem.propTypes = { + project: PropTypes.shape({ + web_url: PropTypes.string.isRequired + }).isRequired, + pipeline: PropTypes.shape({ + id: PropTypes.number.isRequired, + status: PropTypes.string.isRequired, + }).isRequired +}; + +export default PipelineHistoryItem; \ No newline at end of file diff --git a/src/components/index.js b/src/components/index.js index 8998f67..4ad9153 100644 --- a/src/components/index.js +++ b/src/components/index.js @@ -5,7 +5,9 @@ import BuildHistory from './BuildHistory.jsx'; import BuildHistogram from './BuildHistogram.jsx'; import Branches from './Branches.jsx'; import MergeRequestsGauge from './MergeRequestsGauge.jsx'; - +import PipelinesHistory from './PipelinesHistory.jsx'; +import MergeRequests from './MergeRequests.jsx'; +import MergeRequestsGaugeGroups from './MergeRequestGaugeGroups.jsx'; export default { Project, @@ -14,5 +16,8 @@ export default { BuildHistory, BuildHistogram, Branches, - MergeRequestsGauge + MergeRequestsGauge, + PipelinesHistory, + MergeRequests, + MergeRequestsGaugeGroups };