├── .eslintignore
├── client.js
├── .babelrc
├── preview
├── project.png
├── gitlab_branches.png
├── project_members.png
├── gitlab_labels_pie.png
├── sample_dashboard.png
├── gitlab_build_history.png
├── gitlab_labels_bubble.png
├── gitlab_build_histogram.png
├── gitlab_labels_treemap.png
├── latest_project_pipeline.png
├── gitlab_merge_requests_gauge.png
└── gitlab_project_contributors.png
├── commitlint.config.js
├── test
├── .eslintrc.yml
└── components
│ ├── __mocks__
│ └── react-measure.js
│ ├── labels
│ ├── LabelsBubble.test.js
│ ├── LabelsTreemap.test.js
│ ├── sampleData.js
│ └── __snapshots__
│ │ ├── LabelsTreemap.test.js.snap
│ │ └── LabelsBubble.test.js.snap
│ ├── Branches.test.js
│ └── __snapshots__
│ └── Branches.test.js.snap
├── .gitignore
├── .prettierrc.yml
├── CHANGELOG.md
├── src
├── components
│ ├── labels
│ │ ├── index.js
│ │ ├── counts.js
│ │ ├── LabelsBubble.js
│ │ ├── LabelsTreemap.js
│ │ ├── LabelsPie.js
│ │ └── LabelsChart.js
│ ├── index.js
│ ├── activity
│ │ ├── EventDescription.js
│ │ ├── ProjectActivityItem.js
│ │ ├── propTypes.js
│ │ ├── EventTargetLink.js
│ │ └── ProjectActivity.js
│ ├── ProjectContributorsItem.js
│ ├── ProjectMembersItem.js
│ ├── JobHistory.js
│ ├── JobHistoryItem.js
│ ├── Branches.js
│ ├── ProjectMembers.js
│ ├── BranchesItem.js
│ ├── ProjectContributors.js
│ ├── ProjectMilestones.js
│ ├── MergeRequestsGauge.js
│ ├── JobHistogram.js
│ ├── Project.js
│ └── pipelines
│ │ └── LatestProjectPipeline.js
└── client
│ ├── config.js
│ ├── index.js
│ └── gitlab.js
├── .lintstagedrc
├── .travis.yml
├── fixtures
├── index.js
├── project.json
├── milestones.json
├── branches.json
└── project_events.json
├── .eslintrc.yml
├── .npmignore
├── LICENSE.md
├── .github
└── ISSUE_TEMPLATE.md
├── package.json
└── README.md
/.eslintignore:
--------------------------------------------------------------------------------
1 | *.snap
--------------------------------------------------------------------------------
/client.js:
--------------------------------------------------------------------------------
1 | module.exports = require('./lib/client')
2 |
--------------------------------------------------------------------------------
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@mozaik/babel-preset"]
3 | }
4 |
--------------------------------------------------------------------------------
/preview/project.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plouc/mozaik-ext-gitlab/HEAD/preview/project.png
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ['@commitlint/config-conventional'],
3 | }
4 |
--------------------------------------------------------------------------------
/test/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | globals:
2 | test: true
3 | expect: true
4 | describe: true
5 | it: true
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .idea
3 | *.log*
4 | /.nyc_output
5 | /coverage
6 | /lib
7 | /es
8 | /node_modules
--------------------------------------------------------------------------------
/preview/gitlab_branches.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plouc/mozaik-ext-gitlab/HEAD/preview/gitlab_branches.png
--------------------------------------------------------------------------------
/preview/project_members.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plouc/mozaik-ext-gitlab/HEAD/preview/project_members.png
--------------------------------------------------------------------------------
/preview/gitlab_labels_pie.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plouc/mozaik-ext-gitlab/HEAD/preview/gitlab_labels_pie.png
--------------------------------------------------------------------------------
/preview/sample_dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plouc/mozaik-ext-gitlab/HEAD/preview/sample_dashboard.png
--------------------------------------------------------------------------------
/preview/gitlab_build_history.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plouc/mozaik-ext-gitlab/HEAD/preview/gitlab_build_history.png
--------------------------------------------------------------------------------
/preview/gitlab_labels_bubble.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plouc/mozaik-ext-gitlab/HEAD/preview/gitlab_labels_bubble.png
--------------------------------------------------------------------------------
/preview/gitlab_build_histogram.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plouc/mozaik-ext-gitlab/HEAD/preview/gitlab_build_histogram.png
--------------------------------------------------------------------------------
/preview/gitlab_labels_treemap.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plouc/mozaik-ext-gitlab/HEAD/preview/gitlab_labels_treemap.png
--------------------------------------------------------------------------------
/preview/latest_project_pipeline.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plouc/mozaik-ext-gitlab/HEAD/preview/latest_project_pipeline.png
--------------------------------------------------------------------------------
/preview/gitlab_merge_requests_gauge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plouc/mozaik-ext-gitlab/HEAD/preview/gitlab_merge_requests_gauge.png
--------------------------------------------------------------------------------
/preview/gitlab_project_contributors.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/plouc/mozaik-ext-gitlab/HEAD/preview/gitlab_project_contributors.png
--------------------------------------------------------------------------------
/.prettierrc.yml:
--------------------------------------------------------------------------------
1 | printWidth: 100
2 | semi: false
3 | tabWidth: 4
4 | singleQuote: true
5 | bracketSpacing: true
6 | trailingComma: es5
7 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # mozaik-ext-gitlab Changelog
2 |
3 | > **Tags:**
4 | > - [New Feature]
5 | > - [Bug Fix]
6 | > - [Breaking Change]
7 | > - [Documentation]
8 | > - [Internal]
9 | > - [Polish]
10 |
--------------------------------------------------------------------------------
/src/components/labels/index.js:
--------------------------------------------------------------------------------
1 | export { default as LabelsBubble } from './LabelsBubble'
2 | export { default as LabelsPie } from './LabelsPie'
3 | export { default as LabelsTreemap } from './LabelsTreemap'
4 |
--------------------------------------------------------------------------------
/.lintstagedrc:
--------------------------------------------------------------------------------
1 | {
2 | "*.js": [
3 | "eslint --fix ./src/** ./test/**",
4 | "prettier --color --write \"{src,test}/**/*.js\" 'client.js'",
5 | "git add",
6 | "jest --bail --findRelatedTests"
7 | ]
8 | }
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - '8'
4 | - '10'
5 | script:
6 | - yarn run lint
7 | - yarn run fmt:check
8 | - yarn run test:cover
9 | after_success:
10 | - yarn run coverage
11 |
--------------------------------------------------------------------------------
/fixtures/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | module.exports = {
4 | branches: require('./branches'),
5 | milestones: require('./milestones'),
6 | project: require('./project'),
7 | projectEvents: require('./project_events'),
8 | }
9 |
--------------------------------------------------------------------------------
/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | parser: babel-eslint
2 | parserOptions:
3 | ecmaVersion: 6
4 | sourceType: module
5 | ecmaFeatures:
6 | jsx: true
7 | env:
8 | browser: true
9 | node: true
10 | es6: true
11 | extends:
12 | - eslint:recommended
13 | - plugin:react/recommended
14 |
--------------------------------------------------------------------------------
/src/components/labels/counts.js:
--------------------------------------------------------------------------------
1 | export const labelByCountType = {
2 | open_issues_count: 'open issues',
3 | closed_issues_count: 'closed issues',
4 | open_merge_requests_count: 'opened merge requests',
5 | }
6 |
7 | export const countTypes = Object.keys(labelByCountType)
8 |
9 | export const countLabel = countType => labelByCountType[countType]
10 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | # Doc
2 | /preview
3 |
4 | # config
5 | .babelrc
6 | .eslintignore
7 | .eslintrc.yml
8 | .gitignore
9 | .lintstagedrc
10 | .npmignore
11 | .prettierignore
12 | .prettierrc.yml
13 | .travis.yml
14 | commitlint.config.js
15 |
16 | # Logs
17 | *.log*
18 |
19 | # Non transpiled source code
20 | /src
21 |
22 | # Tests
23 | /test
24 | /.nyc_output
25 | /coverage
26 |
27 | # GitHub
28 | /.github
29 |
30 | # OSX
31 | .DS_Store
32 |
33 | # IDE
34 | .idea
--------------------------------------------------------------------------------
/src/client/config.js:
--------------------------------------------------------------------------------
1 | const convict = require('convict')
2 |
3 | const config = convict({
4 | gitlab: {
5 | baseUrl: {
6 | doc: 'The gitlab API base url.',
7 | default: null,
8 | format: String,
9 | env: 'GITLAB_BASE_URL',
10 | },
11 | token: {
12 | doc: 'The gitlab API token.',
13 | default: null,
14 | format: String,
15 | env: 'GITLAB_API_TOKEN',
16 | },
17 | },
18 | })
19 |
20 | module.exports = config
21 |
--------------------------------------------------------------------------------
/test/components/__mocks__/react-measure.js:
--------------------------------------------------------------------------------
1 | import { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | export default class MockedMeasure extends Component {
5 | static propTypes = {
6 | onResize: PropTypes.func.isRequired,
7 | children: PropTypes.func.isRequired,
8 | }
9 |
10 | componentDidMount() {
11 | const { onResize } = this.props
12 | onResize({
13 | bounds: {
14 | width: 600,
15 | height: 400,
16 | },
17 | })
18 | }
19 |
20 | render() {
21 | const { children } = this.props
22 |
23 | return children({ measureRef: () => {} })
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/index.js:
--------------------------------------------------------------------------------
1 | import Project from './Project'
2 | import ProjectMembers from './ProjectMembers'
3 | import ProjectContributors from './ProjectContributors'
4 | import JobHistory from './JobHistory'
5 | import JobHistogram from './JobHistogram'
6 | import Branches from './Branches'
7 | import ProjectActivity from './activity/ProjectActivity'
8 | import ProjectMilestones from './ProjectMilestones'
9 | import LatestProjectPipeline from './pipelines/LatestProjectPipeline'
10 | import * as labels from './labels'
11 |
12 | export default {
13 | Project,
14 | ProjectMembers,
15 | ProjectContributors,
16 | JobHistory,
17 | JobHistogram,
18 | Branches,
19 | ProjectActivity,
20 | ProjectMilestones,
21 | LatestProjectPipeline,
22 | ...labels,
23 | }
24 |
--------------------------------------------------------------------------------
/test/components/labels/LabelsBubble.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import renderer from 'react-test-renderer'
3 | import { ThemeProvider } from 'styled-components'
4 | import { defaultTheme } from '@mozaik/ui'
5 | import LabelsBubble from './../../../src/components/labels/LabelsBubble'
6 | import { sampleProject, sampleLabels } from './sampleData'
7 |
8 | test('should render as expected', () => {
9 | const tree = renderer.create(
10 |
11 |
16 |
17 | )
18 |
19 | expect(tree).toMatchSnapshot()
20 | })
21 |
--------------------------------------------------------------------------------
/test/components/labels/LabelsTreemap.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import renderer from 'react-test-renderer'
3 | import { ThemeProvider } from 'styled-components'
4 | import { defaultTheme } from '@mozaik/ui'
5 | import LabelsTreemap from './../../../src/components/labels/LabelsTreemap'
6 | import { sampleProject, sampleLabels } from './sampleData'
7 |
8 | test('should render as expected', () => {
9 | const tree = renderer.create(
10 |
11 |
16 |
17 | )
18 |
19 | expect(tree).toMatchSnapshot()
20 | })
21 |
--------------------------------------------------------------------------------
/test/components/labels/sampleData.js:
--------------------------------------------------------------------------------
1 | export const sampleProject = {
2 | name: 'ploucifier',
3 | web_url: 'https://gitlab.com/plouc/ploucifier',
4 | }
5 |
6 | export const sampleLabels = [
7 | {
8 | name: 'label A',
9 | id: 1,
10 | color: '#F00',
11 | open_issues_count: 21,
12 | closed_issues_count: 7,
13 | open_merge_requests_count: 12,
14 | },
15 | {
16 | name: 'label B',
17 | id: 2,
18 | color: '#F0F',
19 | open_issues_count: 15,
20 | closed_issues_count: 13,
21 | open_merge_requests_count: 9,
22 | },
23 | {
24 | name: 'label C',
25 | id: 3,
26 | color: '#00F',
27 | open_issues_count: 11,
28 | closed_issues_count: 20,
29 | open_merge_requests_count: 3,
30 | },
31 | ]
32 |
--------------------------------------------------------------------------------
/src/components/activity/EventDescription.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { truncate as trunc } from 'lodash'
3 | import { eventPropType } from './propTypes'
4 |
5 | const truncate = str => trunc(str, { length: 84 })
6 |
7 | export default class EventDescription extends Component {
8 | static propTypes = eventPropType
9 |
10 | render() {
11 | const { target_type, target_title, note, push_data } = this.props
12 |
13 | if (target_type === 'Note' || target_type === 'DiffNote') {
14 | return
{truncate(note.body)}
15 | }
16 |
17 | if (target_type === 'Issue' || target_type === 'MergeRequest') {
18 | return {truncate(target_title)}
19 | }
20 |
21 | if (push_data !== undefined) {
22 | return {truncate(push_data.commit_title)}
23 | }
24 |
25 | return null
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright (c) 2016 Raphaël Benitte
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7 | of the Software, and to permit persons to whom the Software is furnished to do
8 | so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
--------------------------------------------------------------------------------
/src/components/activity/ProjectActivityItem.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment, Component } from 'react'
2 | import { WidgetListItem, WidgetAvatar } from '@mozaik/ui'
3 | import { eventPropType } from './propTypes'
4 | import EventTargetLink from './EventTargetLink'
5 | import EventDescription from './EventDescription'
6 |
7 | export default class ProjectActivityItem extends Component {
8 | static propTypes = eventPropType
9 |
10 | render() {
11 | const { action_name, author } = this.props
12 |
13 | return (
14 |
17 | {author.name} {action_name}{' '}
18 |
19 |
20 | }
21 | meta={}
22 | pre={
23 |
24 |
25 |
26 | }
27 | align="top"
28 | />
29 | )
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 | item | info | notes
2 | ------------------------------|------------|---------------------------------------------------------------------------------------------
3 | **node** version | | *output from `node --version`*
4 | **npm** version | | *output from `npm --version`*
5 | **mozaik-ext-github** version | | *available in project's `package.json`*
6 | **mozaik** version | | *available in project's `package.json`*
7 | **mozaik-demo** version | | *version of the demo used, depends on which method you used to setup your Mozaïk dashboard*
8 | **component** | | *name of the extension's component or `client` if it's related to the extension's client*
9 | **browser** | | *browser used, applyable if the issue is not related to the client*
10 |
11 | ## Expected behavior
12 |
13 | *Description of the expected behavior.*
14 |
15 | ## Actual behavior
16 |
17 | *Description of the actual behavior.*
18 |
19 | ## Steps to reproduce
20 |
21 | *Description of the various steps required to reproduce the error.*
22 |
--------------------------------------------------------------------------------
/src/components/ProjectContributorsItem.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { WidgetListItem } from '@mozaik/ui'
4 | import styled from 'styled-components'
5 |
6 | const Additions = styled.span`
7 | color: ${props => props.theme.colors.success};
8 | `
9 |
10 | const Deletions = styled.span`
11 | color: ${props => props.theme.colors.failure};
12 | `
13 |
14 | export default class ProjectContributorsItem extends Component {
15 | static propTypes = {
16 | contributor: PropTypes.shape({
17 | name: PropTypes.string.isRequired,
18 | commits: PropTypes.number.isRequired,
19 | }).isRequired,
20 | }
21 |
22 | render() {
23 | const {
24 | contributor: { name, commits, additions, deletions },
25 | } = this.props
26 |
27 | return (
28 |
32 | {commits} commit {additions} ++{' '}
33 | {deletions} --
34 |
35 | }
36 | />
37 | )
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/components/ProjectMembersItem.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { WidgetListItem, WidgetAvatar, ExternalLink } from '@mozaik/ui'
4 |
5 | export default class ProjectMembersItem extends Component {
6 | static propTypes = {
7 | member: PropTypes.shape({
8 | name: PropTypes.string.isRequired,
9 | username: PropTypes.string.isRequired,
10 | avatar_url: PropTypes.string,
11 | web_url: PropTypes.string.isRequired,
12 | state: PropTypes.string.isRequired,
13 | }).isRequired,
14 | }
15 |
16 | render() {
17 | const {
18 | member: { name, username, avatar_url, web_url, state },
19 | } = this.props
20 |
21 | let avatar = null
22 | if (avatar_url) {
23 | avatar = (
24 |
25 |
26 |
27 | )
28 | }
29 |
30 | return (
31 | {name}}
33 | href={web_url}
34 | pre={avatar}
35 | meta={`@${username} - ${state}`}
36 | align="top"
37 | />
38 | )
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/activity/propTypes.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types'
2 |
3 | export const eventPropType = {
4 | project_id: PropTypes.number.isRequired,
5 | action_name: PropTypes.string.isRequired,
6 | target_type: PropTypes.oneOf([
7 | 'Issue',
8 | 'Milestone',
9 | 'MergeRequest',
10 | 'Note',
11 | 'Project',
12 | 'Snippet',
13 | 'User',
14 | 'DiscussionNote',
15 | 'DiffNote',
16 | ]),
17 | target_id: PropTypes.number,
18 | target_iid: PropTypes.number,
19 | target_title: PropTypes.string,
20 | created_at: PropTypes.string.isRequired,
21 | author: PropTypes.shape({
22 | avatar_url: PropTypes.string.isRequired,
23 | name: PropTypes.string.isRequired,
24 | web_url: PropTypes.string.isRequired,
25 | }).isRequired,
26 | note: PropTypes.shape({
27 | noteable_type: PropTypes.oneOf(['Issue', 'MergeRequest']).isRequired,
28 | }),
29 | push_data: PropTypes.shape({
30 | action: PropTypes.string.isRequired,
31 | commit_count: PropTypes.number.isRequired,
32 | commit_from: PropTypes.string.isRequired,
33 | commit_to: PropTypes.string.isRequired,
34 | commit_title: PropTypes.string.isRequired,
35 | ref_type: PropTypes.string.isRequired,
36 | ref: PropTypes.string.isRequired,
37 | }),
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/labels/LabelsBubble.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { ResponsiveBubble } from 'nivo'
3 | import LabelsChart from './LabelsChart'
4 |
5 | const margin = {
6 | top: 10,
7 | right: 10,
8 | bottom: 10,
9 | left: 10,
10 | }
11 |
12 | export default class LabelsBubble extends Component {
13 | static getApiRequest = LabelsChart.getApiRequest
14 |
15 | render() {
16 | return (
17 |
18 | {({ labels, countBy, animate }) => {
19 | const data = {
20 | name: 'labels',
21 | color: '#000',
22 | children: labels,
23 | }
24 |
25 | return (
26 | `${d.name} ${d[countBy]}`}
32 | labelTextColor="inherit:darker(1.6)"
33 | leavesOnly={true}
34 | colorBy={d => d.color}
35 | animate={animate}
36 | />
37 | )
38 | }}
39 |
40 | )
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/components/labels/LabelsTreemap.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { ResponsiveTreeMap } from 'nivo'
3 | import LabelsChart from './LabelsChart'
4 |
5 | export default class LabelsTreemap extends Component {
6 | static getApiRequest = LabelsChart.getApiRequest
7 |
8 | render() {
9 | return (
10 |
11 | {({ labels, countBy, animate }) => {
12 | const data = {
13 | name: 'labels',
14 | color: '#000',
15 | children: labels,
16 | }
17 |
18 | return (
19 | `${d.name} ${d[countBy]}`}
26 | labelTextColor="inherit:darker(1.6)"
27 | leavesOnly={true}
28 | outerPadding={10}
29 | innerPadding={2}
30 | colorBy={d => d.color}
31 | animate={animate}
32 | />
33 | )
34 | }}
35 |
36 | )
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/components/labels/LabelsPie.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { ResponsivePie } from 'nivo'
3 | import LabelsChart from './LabelsChart'
4 |
5 | const margin = {
6 | top: 30,
7 | right: 60,
8 | bottom: 30,
9 | left: 60,
10 | }
11 |
12 | const skipAngle = 5
13 |
14 | export default class LabelsPie extends Component {
15 | static getApiRequest = LabelsChart.getApiRequest
16 |
17 | render() {
18 | return (
19 |
20 | {({ labels, countBy, animate, theme }) => {
21 | const data = labels.map(label => ({
22 | ...label,
23 | id: `${label.id}`,
24 | label: label.name,
25 | value: label[countBy],
26 | }))
27 |
28 | return (
29 | d.color}
33 | innerRadius={0.6}
34 | padAngle={1}
35 | radialLabel="name"
36 | radialLabelsSkipAngle={skipAngle}
37 | slicesLabel={countBy}
38 | slicesLabelsSkipAngle={skipAngle}
39 | slicesLabelsTextColor="inherit:darker(1.6)"
40 | animate={animate}
41 | theme={theme}
42 | />
43 | )
44 | }}
45 |
46 | )
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/activity/EventTargetLink.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import { eventPropType } from './propTypes'
3 |
4 | export default class EventTargetLink extends Component {
5 | static propTypes = eventPropType
6 |
7 | render() {
8 | const { target_type, target_id, target_iid, note, push_data } = this.props
9 |
10 | if (target_type === 'Issue') {
11 | return (
12 |
13 | {target_type} #{target_iid}
14 |
15 | )
16 | }
17 |
18 | if (target_type === 'MergeRequest') {
19 | return (
20 |
21 | {target_type} !{target_iid}
22 |
23 | )
24 | }
25 |
26 | if (target_type === 'Note' || target_type === 'DiffNote') {
27 | if (note.noteable_type === 'Issue') {
28 | return (
29 |
30 | {note.noteable_type} #{note.noteable_iid}
31 |
32 | )
33 | }
34 |
35 | if (note.noteable_type === 'MergeRequest') {
36 | return (
37 |
38 | {note.noteable_type} !{note.noteable_iid}
39 |
40 | )
41 | }
42 | }
43 |
44 | if (push_data !== undefined) {
45 | return (
46 |
47 | {push_data.ref_type} {push_data.ref}
48 |
49 | )
50 | }
51 |
52 | return (
53 |
54 | {target_type} {target_id} {target_iid}
55 |
56 | )
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/src/components/JobHistory.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import {
4 | TrapApiError,
5 | Widget,
6 | WidgetHeader,
7 | WidgetBody,
8 | WidgetLoader,
9 | ExternalLink,
10 | BarChartIcon,
11 | } from '@mozaik/ui'
12 | import JobHistoryItem from './JobHistoryItem'
13 |
14 | export default class JobHistory extends Component {
15 | static propTypes = {
16 | project: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
17 | title: PropTypes.string,
18 | apiData: PropTypes.shape({
19 | project: PropTypes.shape({
20 | name: PropTypes.string.isRequired,
21 | web_url: PropTypes.string.isRequired,
22 | }).isRequired,
23 | jobs: PropTypes.shape({
24 | items: PropTypes.array.isRequired,
25 | }).isRequired,
26 | }),
27 | apiError: PropTypes.object,
28 | }
29 |
30 | static getApiRequest({ project }) {
31 | return {
32 | id: `gitlab.projectJobs.${project}`,
33 | params: { project },
34 | }
35 | }
36 |
37 | render() {
38 | const { title, apiData, apiError } = this.props
39 |
40 | let body =
41 | let subject = null
42 | if (apiData) {
43 | const { project, jobs } = apiData
44 |
45 | subject = {project.name}
46 |
47 | body = (
48 |
49 | {jobs.items.map(job => (
50 |
51 | ))}
52 |
53 | )
54 | }
55 |
56 | return (
57 |
58 |
63 |
64 | {body}
65 |
66 |
67 | )
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/components/JobHistoryItem.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import moment from 'moment'
4 | import { WidgetListItem, WidgetLabel, WidgetStatusChip, ClockIcon, ExternalLink } from '@mozaik/ui'
5 |
6 | export default class JobHistoryItem extends Component {
7 | static propTypes = {
8 | project: PropTypes.shape({
9 | web_url: PropTypes.string.isRequired,
10 | }).isRequired,
11 | job: PropTypes.shape({
12 | id: PropTypes.number.isRequired,
13 | status: PropTypes.string.isRequired,
14 | finished_at: PropTypes.string,
15 | commit: PropTypes.shape({
16 | message: PropTypes.string.isRequired,
17 | }),
18 | }).isRequired,
19 | }
20 |
21 | render() {
22 | const { project, job } = this.props
23 |
24 | return (
25 |
26 |
29 |
33 | #{job.id}
34 |
35 |
36 |
37 |
38 |
39 |
40 | {job.commit && {job.commit.message}}
41 |
42 | }
43 | meta={
44 | job.finished_at && (
45 |
55 | )
56 | }
57 | pre={}
58 | />
59 |
60 | )
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/fixtures/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": 250833,
3 | "description": "GitLab Runner",
4 | "name": "gitlab-runner",
5 | "name_with_namespace": "GitLab.org / gitlab-runner",
6 | "path": "gitlab-runner",
7 | "path_with_namespace": "gitlab-org/gitlab-runner",
8 | "created_at": "2015-04-27T21:10:25.322Z",
9 | "default_branch": "master",
10 | "tag_list": [],
11 | "ssh_url_to_repo": "git@gitlab.com:gitlab-org/gitlab-runner.git",
12 | "http_url_to_repo": "https://gitlab.com/gitlab-org/gitlab-runner.git",
13 | "web_url": "https://gitlab.com/gitlab-org/gitlab-runner",
14 | "readme_url": "https://gitlab.com/gitlab-org/gitlab-runner/blob/master/README.md",
15 | "avatar_url": "https://assets.gitlab-static.net/uploads/-/system/project/avatar/250833/runner_logo.png",
16 | "star_count": 973,
17 | "forks_count": 762,
18 | "last_activity_at": "2018-07-27T22:14:44.591Z",
19 | "_links": {
20 | "self": "https://gitlab.com/api/v4/projects/250833",
21 | "issues": "https://gitlab.com/api/v4/projects/250833/issues",
22 | "merge_requests": "https://gitlab.com/api/v4/projects/250833/merge_requests",
23 | "repo_branches": "https://gitlab.com/api/v4/projects/250833/repository/branches",
24 | "labels": "https://gitlab.com/api/v4/projects/250833/labels",
25 | "events": "https://gitlab.com/api/v4/projects/250833/events",
26 | "members": "https://gitlab.com/api/v4/projects/250833/members"
27 | },
28 | "archived": false,
29 | "visibility": "public",
30 | "resolve_outdated_diff_discussions": false,
31 | "container_registry_enabled": true,
32 | "issues_enabled": true,
33 | "merge_requests_enabled": true,
34 | "wiki_enabled": false,
35 | "jobs_enabled": true,
36 | "snippets_enabled": false,
37 | "shared_runners_enabled": true,
38 | "lfs_enabled": true,
39 | "creator_id": 12452,
40 | "namespace": {
41 | "id": 9970,
42 | "name": "GitLab.org",
43 | "path": "gitlab-org",
44 | "kind": "group",
45 | "full_path": "gitlab-org",
46 | "parent_id": null
47 | },
48 | "import_status": "finished",
49 | "open_issues_count": 675,
50 | "public_jobs": true,
51 | "ci_config_path": null,
52 | "shared_with_groups": [],
53 | "only_allow_merge_if_pipeline_succeeds": true,
54 | "request_access_enabled": false,
55 | "only_allow_merge_if_all_discussions_are_resolved": false,
56 | "printing_merge_request_link_enabled": true,
57 | "merge_method": "merge",
58 | "permissions": {
59 | "project_access": null,
60 | "group_access": null
61 | },
62 | "approvals_before_merge": 1
63 | }
--------------------------------------------------------------------------------
/src/components/activity/ProjectActivity.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment, Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import {
4 | TrapApiError,
5 | Widget,
6 | WidgetHeader,
7 | WidgetBody,
8 | WidgetLoader,
9 | ExternalLink,
10 | ActivityIcon,
11 | } from '@mozaik/ui'
12 | import ProjectActivityItem from './ProjectActivityItem'
13 | import { eventPropType } from './propTypes'
14 |
15 | export default class ProjectActivity extends Component {
16 | static propTypes = {
17 | project: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
18 | title: PropTypes.string,
19 | apiData: PropTypes.shape({
20 | project: PropTypes.shape({
21 | name: PropTypes.string.isRequired,
22 | web_url: PropTypes.string.isRequired,
23 | }).isRequired,
24 | events: PropTypes.shape({
25 | items: PropTypes.arrayOf(PropTypes.shape(eventPropType)).isRequired,
26 | pagination: PropTypes.shape({
27 | total: PropTypes.number.isRequired,
28 | }).isRequired,
29 | }).isRequired,
30 | }),
31 | apiError: PropTypes.object,
32 | }
33 |
34 | static getApiRequest({ project }) {
35 | return {
36 | id: `gitlab.projectEvents.${project}`,
37 | params: { project },
38 | }
39 | }
40 |
41 | render() {
42 | const { title, apiData, apiError } = this.props
43 |
44 | let body =
45 | let subject = null
46 | if (apiData) {
47 | const { project, events } = apiData
48 |
49 | subject = {project.name}
50 |
51 | body = (
52 |
53 | {events.items.map((event, i) => (
54 |
55 | ))}
56 |
57 | )
58 | }
59 |
60 | return (
61 |
62 |
67 |
68 | {body}
69 |
70 |
71 | )
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/src/components/Branches.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment, Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import {
4 | TrapApiError,
5 | Widget,
6 | WidgetHeader,
7 | WidgetBody,
8 | WidgetLoader,
9 | ExternalLink,
10 | GitBranchIcon,
11 | } from '@mozaik/ui'
12 | import BranchesItem from './BranchesItem'
13 |
14 | export default class Branches extends Component {
15 | static propTypes = {
16 | project: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
17 | title: PropTypes.string,
18 | apiData: PropTypes.shape({
19 | project: PropTypes.shape({
20 | name: PropTypes.string.isRequired,
21 | web_url: PropTypes.string.isRequired,
22 | }).isRequired,
23 | branches: PropTypes.shape({
24 | items: PropTypes.array.isRequired,
25 | pagination: PropTypes.shape({
26 | total: PropTypes.number.isRequired,
27 | }).isRequired,
28 | }).isRequired,
29 | }),
30 | apiError: PropTypes.object,
31 | }
32 |
33 | static getApiRequest({ project }) {
34 | return {
35 | id: `gitlab.projectBranches.${project}`,
36 | params: { project },
37 | }
38 | }
39 |
40 | render() {
41 | const { title, apiData, apiError } = this.props
42 |
43 | let body =
44 | let subject = null
45 | let count = 0
46 | if (apiData) {
47 | const { project, branches } = apiData
48 |
49 | count = branches.pagination.total
50 |
51 | subject = {project.name}
52 |
53 | body = (
54 |
55 | {branches.items.map(branch => (
56 |
57 | ))}
58 |
59 | )
60 | }
61 |
62 | return (
63 |
64 |
70 |
71 | {body}
72 |
73 |
74 | )
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/components/ProjectMembers.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment, Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import {
4 | TrapApiError,
5 | Widget,
6 | WidgetHeader,
7 | WidgetBody,
8 | WidgetLoader,
9 | ExternalLink,
10 | UsersIcon,
11 | } from '@mozaik/ui'
12 | import ProjectMembersItem from './ProjectMembersItem'
13 |
14 | export default class ProjectMembers extends Component {
15 | static propTypes = {
16 | project: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
17 | title: PropTypes.string,
18 | apiData: PropTypes.shape({
19 | project: PropTypes.shape({
20 | name: PropTypes.string.isRequired,
21 | web_url: PropTypes.string.isRequired,
22 | }).isRequired,
23 | members: PropTypes.shape({
24 | items: PropTypes.array.isRequired,
25 | pagination: PropTypes.shape({
26 | total: PropTypes.number.isRequired,
27 | }).isRequired,
28 | }).isRequired,
29 | }),
30 | apiError: PropTypes.object,
31 | }
32 |
33 | static getApiRequest({ project }) {
34 | return {
35 | id: `gitlab.projectMembers.${project}`,
36 | params: { project },
37 | }
38 | }
39 |
40 | render() {
41 | const { title, apiData, apiError } = this.props
42 |
43 | let body =
44 | let subject = null
45 | let count
46 | if (apiData) {
47 | const { project, members } = apiData
48 |
49 | count = members.pagination.total
50 |
51 | subject = {project.name}
52 |
53 | body = (
54 |
55 | {members.items.map(member => (
56 |
57 | ))}
58 |
59 | )
60 | }
61 |
62 | return (
63 |
64 |
70 |
71 | {body}
72 |
73 |
74 | )
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/components/BranchesItem.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment, Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import moment from 'moment'
4 | import styled from 'styled-components'
5 | import { WidgetListItem, Text, typography, ClockIcon, ExternalLink } from '@mozaik/ui'
6 |
7 | const Header = styled.div`
8 | display: flex;
9 | justify-content: space-between;
10 | `
11 |
12 | const CommitSha = styled.a`
13 | ${props => typography(props.theme, 'mono', 'small')};
14 | `
15 |
16 | export default class BranchesItem extends Component {
17 | static propTypes = {
18 | project: PropTypes.shape({
19 | web_url: PropTypes.string.isRequired,
20 | }).isRequired,
21 | branch: PropTypes.shape({
22 | name: PropTypes.string.isRequired,
23 | protected: PropTypes.bool.isRequired,
24 | commit: PropTypes.shape({
25 | id: PropTypes.string.isRequired,
26 | message: PropTypes.string.isRequired,
27 | author_name: PropTypes.string.isRequired,
28 | committed_date: PropTypes.string.isRequired,
29 | }).isRequired,
30 | }).isRequired,
31 | }
32 |
33 | render() {
34 | const { project, branch } = this.props
35 |
36 | return (
37 |
40 |
41 | {branch.name}
42 |
43 |
44 |
49 | {branch.commit.id.substring(0, 7)}
50 |
51 |
52 | }
53 | meta={
54 |
55 | {branch.commit.message}
56 |
63 |
64 |
65 | {moment(branch.commit.committed_date).fromNow()}
66 |
67 |
68 | }
69 | />
70 | )
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/components/ProjectContributors.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment, Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import _ from 'lodash'
4 | import {
5 | TrapApiError,
6 | Widget,
7 | WidgetHeader,
8 | WidgetBody,
9 | WidgetLoader,
10 | ExternalLink,
11 | UsersIcon,
12 | } from '@mozaik/ui'
13 | import ProjectContributorsItem from './ProjectContributorsItem'
14 |
15 | export default class ProjectContributors extends Component {
16 | static propTypes = {
17 | project: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
18 | title: PropTypes.string,
19 | apiData: PropTypes.shape({
20 | project: PropTypes.shape({
21 | name: PropTypes.string.isRequired,
22 | web_url: PropTypes.string.isRequired,
23 | }).isRequired,
24 | contributors: PropTypes.shape({
25 | items: PropTypes.array.isRequired,
26 | pagination: PropTypes.shape({
27 | total: PropTypes.number.isRequired,
28 | }).isRequired,
29 | }).isRequired,
30 | }),
31 | apiError: PropTypes.object,
32 | }
33 |
34 | static getApiRequest({ project }) {
35 | return {
36 | id: `gitlab.projectContributors.${project}`,
37 | params: { project },
38 | }
39 | }
40 |
41 | render() {
42 | const { title, apiData, apiError } = this.props
43 |
44 | let body =
45 | let subject = null
46 | let count
47 | if (apiData) {
48 | const { project, contributors } = apiData
49 |
50 | const sortedContributors = _.orderBy(contributors.items.slice(), ['commits'], ['desc'])
51 |
52 | count = contributors.pagination.total
53 |
54 | subject = {project.name}
55 |
56 | body = (
57 |
58 | {sortedContributors.map(contributor => (
59 |
63 | ))}
64 |
65 | )
66 | }
67 |
68 | return (
69 |
70 |
76 |
77 | {body}
78 |
79 |
80 | )
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/src/components/ProjectMilestones.js:
--------------------------------------------------------------------------------
1 | import React, { Fragment, Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import {
4 | TrapApiError,
5 | Widget,
6 | WidgetHeader,
7 | WidgetBody,
8 | WidgetLoader,
9 | WidgetListItem,
10 | ExternalLink,
11 | } from '@mozaik/ui'
12 |
13 | export default class ProjectMilestones extends Component {
14 | static propTypes = {
15 | project: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
16 | title: PropTypes.string,
17 | apiData: PropTypes.shape({
18 | project: PropTypes.object.isRequired,
19 | milestones: {
20 | items: PropTypes.arrayOf(
21 | PropTypes.shape({
22 | id: PropTypes.number.isRequired,
23 | title: PropTypes.string.isRequired,
24 | state: PropTypes.string.isRequired,
25 | due_date: PropTypes.string.isRequired,
26 | })
27 | ).isRequired,
28 | pagination: PropTypes.shape({
29 | total: PropTypes.number.isRequired,
30 | }).isRequired,
31 | },
32 | }),
33 | apiError: PropTypes.object,
34 | }
35 |
36 | static getApiRequest({ project }) {
37 | return {
38 | id: `gitlab.projectMilestones.${project}`,
39 | params: { project },
40 | }
41 | }
42 |
43 | render() {
44 | const { title, apiData, apiError } = this.props
45 |
46 | let body =
47 | let subject = null
48 | let count
49 | if (apiData) {
50 | const { project, milestones } = apiData
51 |
52 | count = milestones.pagination.total
53 |
54 | subject = {project.name}
55 |
56 | body = (
57 |
58 | {milestones.items.map(milestone => (
59 |
65 | ))}
66 |
67 | )
68 | }
69 |
70 | return (
71 |
72 |
77 |
78 | {body}
79 |
80 |
81 | )
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/components/labels/LabelsChart.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import {
4 | TrapApiError,
5 | Widget,
6 | WidgetHeader,
7 | WidgetBody,
8 | WidgetLoader,
9 | ExternalLink,
10 | TagIcon,
11 | } from '@mozaik/ui'
12 | import { countTypes, countLabel } from './counts'
13 |
14 | export default class LabelsChart extends Component {
15 | static propTypes = {
16 | project: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
17 | countBy: PropTypes.oneOf(countTypes).isRequired,
18 | apiData: PropTypes.shape({
19 | project: PropTypes.shape({
20 | name: PropTypes.string.isRequired,
21 | web_url: PropTypes.string.isRequired,
22 | }).isRequired,
23 | labels: PropTypes.arrayOf(
24 | PropTypes.shape({
25 | id: PropTypes.number.isRequired,
26 | name: PropTypes.string.isRequired,
27 | color: PropTypes.string.isRequired,
28 | open_issues_count: PropTypes.number.isRequired,
29 | closed_issues_count: PropTypes.number.isRequired,
30 | open_merge_requests_count: PropTypes.number.isRequired,
31 | })
32 | ).isRequired,
33 | }),
34 | apiError: PropTypes.object,
35 | title: PropTypes.string,
36 | animate: PropTypes.bool.isRequired,
37 | children: PropTypes.func.isRequired,
38 | theme: PropTypes.object.isRequired,
39 | }
40 |
41 | static defaultProps = {
42 | countBy: 'open_issues_count',
43 | animate: false,
44 | }
45 |
46 | static getApiRequest({ project }) {
47 | return {
48 | id: `gitlab.projectLabels.${project}`,
49 | params: { project },
50 | }
51 | }
52 |
53 | render() {
54 | const { apiData, apiError, title, countBy, animate, children, theme } = this.props
55 |
56 | let body =
57 | let subject = null
58 | let count = 0
59 | if (apiData) {
60 | const { project, labels } = apiData
61 |
62 | count = labels.length
63 |
64 | subject = {project.name}
65 |
66 | body = children({
67 | labels,
68 | countBy,
69 | animate,
70 | theme: theme.charts,
71 | })
72 | }
73 |
74 | return (
75 |
76 |
82 |
83 | {body}
84 |
85 |
86 | )
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@mozaik/ext-gitlab",
3 | "version": "2.0.0-alpha.9",
4 | "description": "Mozaik GitLab widgets",
5 | "repository": {
6 | "type": "git",
7 | "url": "git://github.com/plouc/mozaik-ext-gitlab"
8 | },
9 | "license": "MIT",
10 | "author": {
11 | "name": "Raphaël Benitte",
12 | "url": "https://github.com/plouc"
13 | },
14 | "homepage": "https://github.com/plouc/mozaik-ext-gitlab",
15 | "main": "./lib/components/index.js",
16 | "module": "es/components/index.js",
17 | "jsnext:main": "es/components/index.js",
18 | "keywords": [
19 | "gitlab",
20 | "gitlab-ci",
21 | "mozaik",
22 | "widget",
23 | "extension",
24 | "dashboard"
25 | ],
26 | "engineStrict": true,
27 | "engines": {
28 | "node": ">=8.2.0",
29 | "npm": ">=3.0.0"
30 | },
31 | "dependencies": {
32 | "chalk": "^2.0.1",
33 | "convict": "^4.0.0",
34 | "lodash": "^4.17.4",
35 | "lodash-es": "^4.17.4",
36 | "moment": "^2.18.1",
37 | "prop-types": "^15.5.10"
38 | },
39 | "devDependencies": {
40 | "@commitlint/cli": "^7.0.0",
41 | "@commitlint/config-conventional": "^7.0.1",
42 | "@mozaik/babel-preset": "^1.0.0-alpha.6",
43 | "@mozaik/ui": "^2.0.0-rc.0",
44 | "babel-cli": "^6.24.1",
45 | "babel-eslint": "^7.2.3",
46 | "babel-jest": "^20.0.3",
47 | "coveralls": "^2.11.15",
48 | "cross-env": "^5.0.1",
49 | "enzyme": "^3.3.0",
50 | "enzyme-adapter-react-16": "^1.1.1",
51 | "eslint": "^5.2.0",
52 | "eslint-plugin-react": "^7.10.0",
53 | "husky": "^0.14.3",
54 | "jest": "^23.4.2",
55 | "lint-staged": "^7.2.0",
56 | "nivo": "^0.12.0",
57 | "nock": "^9.0.14",
58 | "prettier": "^1.14.0",
59 | "react": "^16.4.0",
60 | "react-dom": "^16.4.0",
61 | "react-test-renderer": "^16.4.0",
62 | "styled-components": "^2.1.1"
63 | },
64 | "peerDependencies": {
65 | "@mozaik/ui": "^2.0.0-rc.0",
66 | "nivo": "^0.12.0",
67 | "react": "^16.4.0",
68 | "styled-components": "^2.1.1"
69 | },
70 | "scripts": {
71 | "lint": "eslint ./src/** ./test/**",
72 | "lint:fix": "eslint --fix ./src/** ./test/**",
73 | "test": "jest --verbose",
74 | "test:cover": "jest --verbose --coverage",
75 | "coverage": "cat ./coverage/lcov.info | coveralls",
76 | "build:commonjs": "cross-env BABEL_ENV=commonjs babel src --out-dir lib",
77 | "build:commonjs:watch": "npm run build:commonjs -- --watch",
78 | "build:es": "cross-env BABEL_ENV=es babel src --out-dir es",
79 | "build:es:watch": "npm run build:es -- --watch",
80 | "build": "npm run build:commonjs && npm run build:es",
81 | "fmt": "prettier --color --write \"{src,test}/**/*.js\" 'client.js'",
82 | "fmt:check": "prettier --list-different \"{src,test}/**/*.js\" 'client.js'",
83 | "prepublishOnly": "npm run lint && npm test && npm run build",
84 | "version": "echo ${npm_package_version}",
85 | "precommit": "lint-staged",
86 | "commitmsg": "commitlint -E GIT_PARAMS"
87 | },
88 | "jest": {
89 | "testURL": "http://localhost/"
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/components/MergeRequestsGauge.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import _ from 'lodash'
4 | import { Gauge, Widget, WidgetHeader, WidgetBody } from '@mozaik/ui'
5 |
6 | export default class MergeRequestsGauge extends Component {
7 | static propTypes = {
8 | project: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
9 | thresholds: PropTypes.arrayOf(
10 | PropTypes.shape({
11 | threshold: PropTypes.number.isRequired,
12 | color: PropTypes.string.isRequired,
13 | message: PropTypes.string.isRequired,
14 | })
15 | ).isRequired,
16 | apiData: PropTypes.number.isRequired,
17 | apiError: PropTypes.object,
18 | }
19 |
20 | static defaultProps = {
21 | thresholds: [
22 | { threshold: 3, color: '#85e985', message: 'good job!' },
23 | {
24 | threshold: 5,
25 | color: '#ecc265',
26 | message: 'you should consider reviewing',
27 | },
28 | {
29 | threshold: 10,
30 | color: '#f26a3f',
31 | message: 'merge requests overflow',
32 | },
33 | ],
34 | apiData: 0,
35 | }
36 |
37 | static getApiRequest({ project }) {
38 | return {
39 | id: `gitlab.projectMergeRequests.${project}.opened`,
40 | params: {
41 | project,
42 | query: {
43 | state: 'opened',
44 | },
45 | },
46 | }
47 | }
48 |
49 | render() {
50 | const { thresholds, apiData: mergeRequestCount } = this.props
51 |
52 | const cappedValue = Math.min(
53 | mergeRequestCount,
54 | _.max(thresholds.map(threshold => threshold.threshold))
55 | )
56 | let message = null
57 | const normThresholds = thresholds.map(threshold => {
58 | if (message === null && cappedValue <= threshold.threshold) {
59 | message = threshold.message
60 | }
61 |
62 | return {
63 | upperBound: threshold.threshold,
64 | color: threshold.color,
65 | }
66 | })
67 |
68 | return (
69 |
70 |
75 |
76 |
77 |
83 |
84 | {message}
85 |
86 |
87 | )
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/test/components/Branches.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Enzyme from 'enzyme'
3 | import Adapter from 'enzyme-adapter-react-16'
4 | import renderer from 'react-test-renderer'
5 | import { ThemeProvider } from 'styled-components'
6 | import { WidgetHeader, WidgetLoader, defaultTheme } from '@mozaik/ui'
7 | import Branches from './../../src/components/Branches'
8 | import fixtures from '../../fixtures'
9 |
10 | Enzyme.configure({ adapter: new Adapter() })
11 |
12 | test('should return correct api request', () => {
13 | expect(
14 | Branches.getApiRequest({
15 | project: fixtures.project.name,
16 | })
17 | ).toEqual({
18 | id: `gitlab.projectBranches.${fixtures.project.name}`,
19 | params: { project: fixtures.project.name },
20 | })
21 | })
22 |
23 | test('should display loader if no apiData available', () => {
24 | const wrapper = Enzyme.shallow()
25 |
26 | expect(wrapper.find(WidgetLoader).exists()).toBeTruthy()
27 | })
28 |
29 | test('header should display 0 count by default', () => {
30 | const wrapper = Enzyme.shallow()
31 |
32 | const header = wrapper.find(WidgetHeader)
33 | expect(header.prop('count')).toBe(0)
34 | })
35 |
36 | test('header should display pull request count when api sent data', () => {
37 | const wrapper = Enzyme.shallow(
38 |
45 | )
46 |
47 | const header = wrapper.find(WidgetHeader)
48 | expect(header.exists()).toBeTruthy()
49 | expect(header.prop('count')).toBe(42)
50 | })
51 |
52 | test(`header title should default to ' Branches'`, () => {
53 | const wrapper = Enzyme.shallow(
54 |
61 | )
62 |
63 | const header = wrapper.find(WidgetHeader)
64 | expect(header.prop('title')).toBe('Branches')
65 | })
66 |
67 | test(`header title should be overridden when passing 'title' prop`, () => {
68 | const customTitle = 'Custom Title'
69 | const wrapper = Enzyme.shallow(
70 |
78 | )
79 |
80 | const header = wrapper.find(WidgetHeader)
81 | expect(header.prop('title')).toBe(customTitle)
82 | })
83 |
84 | test('should render as expected', () => {
85 | const tree = renderer.create(
86 |
87 |
94 |
95 | )
96 |
97 | expect(tree).toMatchSnapshot()
98 | })
99 |
--------------------------------------------------------------------------------
/src/components/JobHistogram.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import moment from 'moment'
4 | import {
5 | TrapApiError,
6 | Widget,
7 | WidgetHeader,
8 | WidgetBody,
9 | WidgetLoader,
10 | ExternalLink,
11 | BarChartIcon,
12 | } from '@mozaik/ui'
13 | import { ResponsiveBar } from 'nivo'
14 |
15 | const margin = { top: 20, right: 20, bottom: 60, left: 70 }
16 | const colorBy = d => d.color
17 | const axisLeft = {
18 | tickPadding: 7,
19 | tickSize: 0,
20 | legend: 'duration (mn)',
21 | legendPosition: 'center',
22 | legendOffset: -40,
23 | }
24 | const axisBottom = {
25 | tickSize: 0,
26 | tickPadding: 7,
27 | legend: 'job id',
28 | legendPosition: 'center',
29 | legendOffset: 40,
30 | }
31 |
32 | export default class JobHistogram extends Component {
33 | static propTypes = {
34 | project: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
35 | title: PropTypes.string,
36 | apiData: PropTypes.shape({
37 | project: PropTypes.shape({
38 | name: PropTypes.string.isRequired,
39 | web_url: PropTypes.string.isRequired,
40 | }).isRequired,
41 | jobs: PropTypes.shape({
42 | items: PropTypes.array.isRequired,
43 | }).isRequired,
44 | }),
45 | apiError: PropTypes.object,
46 | theme: PropTypes.object.isRequired,
47 | }
48 |
49 | static getApiRequest({ project }) {
50 | return {
51 | id: `gitlab.projectJobs.${project}`,
52 | params: { project },
53 | }
54 | }
55 |
56 | render() {
57 | const { title, apiData, apiError, theme } = this.props
58 |
59 | const getColorForStatus = status => {
60 | if (status === 'success') return theme.colors.success
61 | if (status === 'failed') return theme.colors.failure
62 | return theme.colors.unknown
63 | }
64 |
65 | let body =
66 | let subject = null
67 | if (apiData) {
68 | const { project, jobs } = apiData
69 | subject = {project.name}
70 |
71 | const data = [
72 | {
73 | id: 'jobs',
74 | data: jobs.items.map(({ id, status, started_at, finished_at }) => {
75 | let duration = 0
76 | if (started_at && finished_at) {
77 | duration = moment(finished_at).diff(started_at, 'minutes')
78 | }
79 |
80 | return {
81 | id: `${id}`,
82 | duration,
83 | status,
84 | x: id,
85 | y: duration,
86 | color: getColorForStatus(status),
87 | }
88 | }),
89 | },
90 | ]
91 |
92 | body = (
93 |
104 | )
105 | }
106 |
107 | return (
108 |
109 |
114 |
115 | {body}
116 |
117 |
118 | )
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/test/components/labels/__snapshots__/LabelsTreemap.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`should render as expected 1`] = `
4 |
8 |
11 |
15 |
16 |
27 | Labels by open issues
28 |
31 | 3
32 |
33 |
34 |
59 |
60 |
64 |
72 |
166 |
167 |
168 |
169 |
170 | `;
171 |
--------------------------------------------------------------------------------
/src/client/index.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const config = require('./config')
4 | const Gitlab = require('./gitlab').Gitlab
5 |
6 | const aggregatePipelineJobs = jobs => {
7 | let stages = []
8 | jobs.forEach(job => {
9 | let stage = stages.find(s => s.name === job.stage)
10 | if (stage === undefined) {
11 | stage = {
12 | name: job.stage,
13 | jobs: [],
14 | status: 'success',
15 | }
16 | stages.push(stage)
17 | }
18 |
19 | stage.jobs.push(job)
20 | })
21 |
22 | stages = stages.map(stage => {
23 | let status
24 | stage.jobs.forEach(job => {
25 | if (job.status === 'failed') {
26 | status = 'failed'
27 | }
28 | if (status === undefined && job.status === 'success') {
29 | status = 'success'
30 | }
31 | if (status === undefined && job.status === 'manual') {
32 | status = 'skipped'
33 | }
34 | })
35 |
36 | if (status === undefined) {
37 | status = 'pending'
38 | }
39 |
40 | stage.status = status
41 |
42 | return stage
43 | })
44 |
45 | return stages
46 | }
47 |
48 | /**
49 | * @param {Mozaik} mozaik
50 | */
51 | module.exports = mozaik => {
52 | mozaik.loadApiConfig(config)
53 |
54 | const gitlab = new Gitlab(
55 | config.get('gitlab.baseUrl'),
56 | config.get('gitlab.token'),
57 | mozaik.request,
58 | mozaik.logger
59 | )
60 |
61 | return {
62 | project({ project }) {
63 | return gitlab.getProject(project)
64 | },
65 | projectMembers({ project }) {
66 | return Promise.all([
67 | gitlab.getProject(project),
68 | gitlab.getProjectMembers(project),
69 | ]).then(([project, members]) => ({
70 | project,
71 | members,
72 | }))
73 | },
74 | projectContributors({ project }) {
75 | return Promise.all([
76 | gitlab.getProject(project),
77 | gitlab.getProjectContributors(project),
78 | ]).then(([project, contributors]) => ({
79 | project,
80 | contributors,
81 | }))
82 | },
83 | projectJobs({ project }) {
84 | return Promise.all([gitlab.getProject(project), gitlab.getProjectJobs(project)]).then(
85 | ([project, jobs]) => ({
86 | project,
87 | jobs,
88 | })
89 | )
90 | },
91 | projectBranches({ project }) {
92 | return Promise.all([
93 | gitlab.getProject(project),
94 | gitlab.getProjectBranches(project),
95 | ]).then(([project, branches]) => ({
96 | project,
97 | branches,
98 | }))
99 | },
100 | projectMergeRequests({ project, query = {} }) {
101 | return gitlab.getProjectMergeRequests(project, query)
102 | },
103 | projectLabels({ project }) {
104 | return Promise.all([gitlab.getProject(project), gitlab.getProjectLabels(project)]).then(
105 | ([project, labels]) => ({
106 | project,
107 | labels,
108 | })
109 | )
110 | },
111 | projectMilestones({ project }) {
112 | return Promise.all([
113 | gitlab.getProject(project),
114 | gitlab.getProjectMilestones(project),
115 | ]).then(([project, milestones]) => ({
116 | project,
117 | milestones,
118 | }))
119 | },
120 | projectEvents({ project }) {
121 | return Promise.all([gitlab.getProject(project), gitlab.getProjectEvents(project)]).then(
122 | ([project, events]) => ({
123 | project,
124 | events,
125 | })
126 | )
127 | },
128 | latestProjectPipeline({ project, ref }) {
129 | return gitlab.getProjectPipelines(project, { ref, per_page: 1 }).then(({ items }) => {
130 | if (items.length === 0) return null
131 |
132 | return Promise.all([
133 | gitlab.getProject(project),
134 | gitlab.getProjectPipeline(project, items[0].id),
135 | gitlab.getProjectPipelineJobs(project, items[0].id, { per_page: 100 }),
136 | ]).then(([project, pipeline, jobs]) => {
137 | let commit
138 | if (jobs.items.length > 0) {
139 | commit = jobs.items[0].commit
140 | }
141 |
142 | return {
143 | ...pipeline,
144 | project,
145 | commit,
146 | stages: aggregatePipelineJobs(jobs.items),
147 | }
148 | })
149 | })
150 | },
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/src/components/Project.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 | import styled from 'styled-components'
4 | import {
5 | TrapApiError,
6 | Widget,
7 | WidgetBody,
8 | WidgetLoader,
9 | WidgetLabel,
10 | ExternalLink,
11 | LockIcon,
12 | UnlockIcon,
13 | StarIcon,
14 | GitBranchIcon,
15 | typography,
16 | WidgetAvatar,
17 | } from '@mozaik/ui'
18 |
19 | const AVATAR_SIZE = '12vmin'
20 | const ICON_SIZE = '1.8vmin'
21 |
22 | const Container = styled.div`
23 | display: flex;
24 | flex-direction: column;
25 | height: 100%;
26 | `
27 |
28 | const Header = styled.div`
29 | flex: 1;
30 | display: flex;
31 | align-items: center;
32 | justify-content: center;
33 | `
34 |
35 | const AvatarPlaceholder = styled.span`
36 | width: ${AVATAR_SIZE};
37 | height: ${AVATAR_SIZE};
38 | display: flex;
39 | align-items: center;
40 | justify-content: center;
41 | text-transform: uppercase;
42 | color: ${props => props.theme.colors.textHighlight};
43 | background: ${props => props.theme.colors.unknown};
44 | ${props => typography(props.theme, 'display')} font-size: 6vmin;
45 | `
46 |
47 | const Name = styled.div`
48 | display: flex;
49 | align-items: center;
50 | justify-content: center;
51 | white-space: pre;
52 | color: ${props => props.theme.colors.textHighlight};
53 | margin: 1vmin 0 3vmin;
54 | ${props => typography(props.theme, 'default', 'strong')};
55 | `
56 |
57 | const Grid = styled.div`
58 | display: grid;
59 | grid-template-columns: 1fr 1fr;
60 | grid-column-gap: 2vmin;
61 | grid-row-gap: 2vmin;
62 | `
63 |
64 | const Count = styled.span`
65 | color: ${props => props.theme.colors.textHighlight};
66 | ${props => typography(props.theme, 'default', 'strong')};
67 | `
68 |
69 | export default class Project extends Component {
70 | static propTypes = {
71 | project: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired,
72 | apiData: PropTypes.shape({
73 | name: PropTypes.string.isRequired,
74 | name_with_namespace: PropTypes.string.isRequired,
75 | visibility: PropTypes.string.isRequired,
76 | avatar_url: PropTypes.string,
77 | star_count: PropTypes.number.isRequired,
78 | forks_count: PropTypes.number.isRequired,
79 | }),
80 | apiError: PropTypes.object,
81 | theme: PropTypes.object.isRequired,
82 | }
83 |
84 | static getApiRequest({ project }) {
85 | return {
86 | id: `gitlab.project.${project}`,
87 | params: { project },
88 | }
89 | }
90 |
91 | render() {
92 | const { apiData: project, apiError, theme } = this.props
93 |
94 | let body =
95 | if (project) {
96 | let visibilityIcon
97 | if (project.visibility === 'public') {
98 | visibilityIcon = (
99 |
104 | )
105 | } else {
106 | visibilityIcon = (
107 |
112 | )
113 | }
114 |
115 | let avatar
116 | if (project.avatar_url !== null) {
117 | avatar =
118 | } else {
119 | avatar = {project.name[0]}
120 | }
121 |
122 | body = (
123 |
124 |
127 |
128 |
129 | {project.name_with_namespace}
130 | {' '}
131 | {visibilityIcon}
132 |
133 |
134 | {project.star_count}}
136 | prefix={}
137 | suffix="stars"
138 | />
139 | {project.forks_count}}
141 | prefix={}
142 | suffix={
143 | forks
144 | }
145 | />
146 |
147 |
148 | )
149 | }
150 |
151 | return (
152 |
153 |
154 | {body}
155 |
156 |
157 | )
158 | }
159 | }
160 |
--------------------------------------------------------------------------------
/test/components/labels/__snapshots__/LabelsBubble.test.js.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`should render as expected 1`] = `
4 |
8 |
11 |
15 |
16 |
27 | Labels by open issues
28 |
31 | 3
32 |
33 |
34 |
59 |
60 |
64 |
72 |
79 |
191 |
192 |
193 |
194 |
195 |
196 | `;
197 |
--------------------------------------------------------------------------------
/src/client/gitlab.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const chalk = require('chalk')
4 |
5 | /**
6 | * @typedef {Object} Pagination
7 | * @property {number} total
8 | * @property {number} page
9 | * @property {number} pages
10 | * @property {number} perPage
11 | * @property {number} previousPage
12 | * @property {number} nextPage
13 | */
14 |
15 | /**
16 | * @typedef {Object} PaginationOptions
17 | * @property {number} [per_page]
18 | * @property {number} [page]
19 | */
20 |
21 | /**
22 | * @typedef {Object} PipelineOptions
23 | * @property {number} [per_page]
24 | * @property {number} [page]
25 | * @property {'running'|'pending'|'finished'|'branches'|'tags'} [scope] - The scope of pipelines, one of: running, pending, finished, branches, tags
26 | * @property {'running'|'pending'|'success'|'failed'|'canceled'|'skipped'} [status] - The status of pipelines, one of: running, pending, success, failed, canceled, skipped
27 | * @property {string} [ref] - The ref of pipelines
28 | * @property {string} [sha] - The sha or pipelines
29 | * @property {boolean} [yaml_errors] - Returns pipelines with invalid configurations
30 | * @property {string} [name] - The name of the user who triggered pipelines
31 | * @property {string} [username] - The username of the user who triggered pipelines
32 | * @property {'id'|'status'|'ref'|'user_id'} [order_by] - Order pipelines by id, status, ref, or user_id (default: id)
33 | * @property {'asc'|'desc'} [sort] - Sort pipelines in asc or desc order (default: desc)
34 | */
35 |
36 | /**
37 | * @param {object} headers
38 | * @return {Pagination}
39 | */
40 | exports.paginationFromHeaders = headers => ({
41 | total: Number(headers['x-total']),
42 | page: Number(headers['x-page']),
43 | pages: Number(headers['x-total-pages']),
44 | perPage: Number(headers['x-per-page']),
45 | previousPage: Number(headers['x-prev-page']),
46 | nextPage: Number(headers['x-next-page']),
47 | })
48 |
49 | class GitLab {
50 | constructor(baseUrl, token, request, logger) {
51 | this.baseUrl = baseUrl
52 | this.token = token
53 | this.request = request
54 | this.logger = logger
55 | }
56 |
57 | makeRequest(path, qs) {
58 | const uri = `${this.baseUrl}${path}`
59 |
60 | const options = {
61 | uri,
62 | qs,
63 | json: true,
64 | resolveWithFullResponse: true,
65 | headers: {
66 | 'PRIVATE-TOKEN': this.token,
67 | },
68 | }
69 |
70 | const paramsDebug = qs ? ` ${JSON.stringify(qs)}` : ''
71 | this.logger.info(chalk.yellow(`[gitlab] calling ${uri}${paramsDebug}`))
72 |
73 | return this.request(options)
74 | }
75 |
76 | getProject(projectId) {
77 | return this.makeRequest(`/projects/${encodeURIComponent(projectId)}`).then(res => res.body)
78 | }
79 |
80 | getProjectMembers(projectId) {
81 | return this.makeRequest(`/projects/${encodeURIComponent(projectId)}/members`).then(res => ({
82 | items: res.body,
83 | pagination: exports.paginationFromHeaders(res.headers),
84 | }))
85 | }
86 |
87 | getProjectContributors(projectId) {
88 | return this.makeRequest(
89 | `/projects/${encodeURIComponent(projectId)}/repository/contributors`
90 | ).then(res => ({
91 | items: res.body,
92 | pagination: exports.paginationFromHeaders(res.headers),
93 | }))
94 | }
95 |
96 | getProjectJobs(projectId) {
97 | return this.makeRequest(`/projects/${encodeURIComponent(projectId)}/jobs`, {
98 | per_page: 40,
99 | }).then(res => ({
100 | items: res.body,
101 | pagination: exports.paginationFromHeaders(res.headers),
102 | }))
103 | }
104 |
105 | getProjectBranches(projectId) {
106 | return this.makeRequest(
107 | `/projects/${encodeURIComponent(projectId)}/repository/branches`
108 | ).then(res => ({
109 | items: res.body,
110 | pagination: exports.paginationFromHeaders(res.headers),
111 | }))
112 | }
113 |
114 | getProjectMergeRequests(projectId, query = {}) {
115 | return this.makeRequest(
116 | `/projects/${encodeURIComponent(projectId)}/merge_requests`,
117 | query
118 | ).then(res => ({
119 | items: res.body,
120 | pagination: exports.paginationFromHeaders(res.headers),
121 | }))
122 | }
123 |
124 | getProjectLabels(projectId) {
125 | return this.makeRequest(`/projects/${encodeURIComponent(projectId)}/labels`).then(res => ({
126 | items: res.body,
127 | pagination: exports.paginationFromHeaders(res.headers),
128 | }))
129 | }
130 |
131 | getProjectMilestones(projectId) {
132 | return this.makeRequest(`/projects/${encodeURIComponent(projectId)}/milestones`).then(
133 | res => ({
134 | items: res.body,
135 | pagination: exports.paginationFromHeaders(res.headers),
136 | })
137 | )
138 | }
139 |
140 | getProjectEvents(projectId) {
141 | return this.makeRequest(`/projects/${encodeURIComponent(projectId)}/events`).then(res => ({
142 | items: res.body,
143 | pagination: exports.paginationFromHeaders(res.headers),
144 | }))
145 | }
146 |
147 | /**
148 | * @param {string|number} projectId
149 | * @param {PipelineOptions} options
150 | *
151 | * @return {Promise