├── src
├── actions
│ ├── index.js
│ └── github.js
├── image
│ ├── logo.png
│ ├── WX20190912-172947.png
│ └── WX20190912-173245.png
├── components
│ ├── sections
│ │ ├── Colors.js
│ │ ├── ChartOptions.js
│ │ ├── Repository.js
│ │ ├── Release.js
│ │ ├── Fork.js
│ │ ├── PullRequests.js
│ │ ├── Commit.js
│ │ ├── Issues.js
│ │ └── Star.js
│ ├── DataTypes.js
│ ├── App.js
│ ├── DataUnit.js
│ ├── GithubStatistics.js
│ └── DataSection.js
├── reducers
│ ├── index.js
│ └── github.js
├── App.test.js
├── css
│ ├── index.css
│ ├── DataSection.css
│ ├── App.css
│ └── normalize.css
├── index.js
├── serviceWorker.js
└── scripts
│ └── GithubFetcher.js
├── .env
├── .vscode
└── settings.json
├── public
├── robots.txt
├── github-sign-16.png
├── github-sign-24.png
├── github-sign-256.png
├── github-sign-32.png
├── github-sign-512.png
├── github-sign-64.png
├── manifest.json
└── index.html
├── .gitignore
├── .eslintrc.json
├── package.json
├── README.md
└── csv.js
/src/actions/index.js:
--------------------------------------------------------------------------------
1 | export * from './github'
--------------------------------------------------------------------------------
/.env:
--------------------------------------------------------------------------------
1 | REACT_APP_GITHUB_API_TOKEN="${YOUR_GITHUB_API_TOKEN}"
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "eslint.enable": true
3 | }
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 |
--------------------------------------------------------------------------------
/src/image/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vesoft-inc/github-statistics/HEAD/src/image/logo.png
--------------------------------------------------------------------------------
/public/github-sign-16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vesoft-inc/github-statistics/HEAD/public/github-sign-16.png
--------------------------------------------------------------------------------
/public/github-sign-24.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vesoft-inc/github-statistics/HEAD/public/github-sign-24.png
--------------------------------------------------------------------------------
/public/github-sign-256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vesoft-inc/github-statistics/HEAD/public/github-sign-256.png
--------------------------------------------------------------------------------
/public/github-sign-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vesoft-inc/github-statistics/HEAD/public/github-sign-32.png
--------------------------------------------------------------------------------
/public/github-sign-512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vesoft-inc/github-statistics/HEAD/public/github-sign-512.png
--------------------------------------------------------------------------------
/public/github-sign-64.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vesoft-inc/github-statistics/HEAD/public/github-sign-64.png
--------------------------------------------------------------------------------
/src/image/WX20190912-172947.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vesoft-inc/github-statistics/HEAD/src/image/WX20190912-172947.png
--------------------------------------------------------------------------------
/src/image/WX20190912-173245.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vesoft-inc/github-statistics/HEAD/src/image/WX20190912-173245.png
--------------------------------------------------------------------------------
/src/components/sections/Colors.js:
--------------------------------------------------------------------------------
1 | export default [
2 | '#A2B449',
3 | '#E89A41',
4 | '#9EABCD',
5 | '#56BABD',
6 | '#D79AB3',
7 | '#E6978A',
8 | '#5EBE85',
9 | ]
--------------------------------------------------------------------------------
/src/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 |
3 | import github from './github'
4 |
5 | const reducers = combineReducers({
6 | github: github,
7 | })
8 |
9 | export default reducers
--------------------------------------------------------------------------------
/src/actions/github.js:
--------------------------------------------------------------------------------
1 | export const updateState = (state, data) => ({
2 | type: 'UPDATE_STATE',
3 | payload: { state, data }
4 | })
5 |
6 | export const updateStatsField = (state, stats) => ({
7 | type: 'UPDATE_STATS_FIELD',
8 | payload: { state, stats }
9 | })
--------------------------------------------------------------------------------
/src/App.test.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './App';
4 |
5 | it('renders without crashing', () => {
6 | const div = document.createElement('div');
7 | ReactDOM.render(, div);
8 | ReactDOM.unmountComponentAtNode(div);
9 | });
10 |
--------------------------------------------------------------------------------
/src/components/DataTypes.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Keys are used for reference
3 | * Values are used for displaying and passing
4 | */
5 |
6 | export default {
7 | REPO: 'Repository',
8 | STAR: 'Star',
9 | FORK: 'Fork',
10 | COMMIT: 'Commit',
11 | RELEASE: 'Release',
12 | ISSUES: 'Issues',
13 | PULLREQUESTS: 'Pull Requests',
14 | }
--------------------------------------------------------------------------------
/src/components/App.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import GithubStatistics from './GithubStatistics'
3 | import '../css/App.css'
4 | class App extends React.Component {
5 | render() {
6 | return (
7 |
8 |
9 |
10 | )
11 | }
12 | }
13 |
14 |
15 |
16 | export default App
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/src/components/sections/ChartOptions.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Common highchart options shared by
3 | * most of the charts
4 | */
5 |
6 | import COLORS from './Colors'
7 |
8 | export default {
9 | title: {
10 | text: undefined,
11 | },
12 | // xAxis: {
13 | // type: 'datetime',
14 | // },
15 | legend: {
16 | itemStyle: {
17 | color: 'rgba(0, 0, 0, 0.85)',
18 | fontWeight: '300'
19 | }
20 | },
21 | colors: COLORS,
22 | tooltip: {
23 | shadow: false,
24 | split: true,
25 | },
26 | credits: {
27 | enabled: false,
28 | },
29 | }
--------------------------------------------------------------------------------
/src/css/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
4 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | overflow: scroll;
9 | white-space: nowrap;
10 | background-color: #fff;
11 |
12 | }
13 |
14 | header {
15 | display: block;
16 | }
17 |
18 | code {
19 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
20 | monospace;
21 | display: block;
22 | }
--------------------------------------------------------------------------------
/src/css/DataSection.css:
--------------------------------------------------------------------------------
1 | .section-header {
2 | margin: 16px 0 0 0;
3 | padding: 8px;
4 | }
5 |
6 | .section-title {
7 | display: inline;
8 | margin: 0 16px;
9 | color: rgba(0, 0, 0, 0.85);
10 | font-weight: 600;
11 | font-size: 24px;
12 | }
13 |
14 | .info-tag {
15 | display: inline;
16 | font-size: 18px;
17 | margin: auto 0;
18 | }
19 |
20 | .data-card {
21 | padding: 12px 12px;
22 | }
23 |
24 | .stats-card {
25 | display: inline-block;
26 | margin: 0 12px 0 0;
27 | padding: 6px 12px 12px 12px;
28 | }
29 |
30 | .ant-tag.repo-tag {
31 | border-width: 0;
32 | background: 0%;
33 | }
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 |
4 | import { Provider } from 'react-redux'
5 | import { createStore } from 'redux'
6 | import reducers from './reducers'
7 |
8 | import './css/normalize.css'
9 | import './css/index.css'
10 | import App from './components/App'
11 | import * as serviceWorker from './serviceWorker'
12 |
13 | const store = createStore(reducers)
14 | ReactDOM.render(
15 |
16 |
17 |
18 |
19 |
20 | ,
21 | document.getElementById('root')
22 | )
23 |
24 | // If you want your app to work offline and load faster, you can change
25 | // unregister() to register() below. Note this comes with some pitfalls.
26 | // Learn more about service workers: https://bit.ly/CRA-PWA
27 | serviceWorker.unregister()
28 |
--------------------------------------------------------------------------------
/src/reducers/github.js:
--------------------------------------------------------------------------------
1 | const INITIAL_STATE = {
2 | repoData: [],
3 | repoStats: {},
4 |
5 | starData: [],
6 | starStats: {},
7 |
8 | forkData: [],
9 | forkStats: {},
10 |
11 | releaseData: [],
12 | releaseStats: {},
13 |
14 | githubApiToken: btoa(process.env.REACT_APP_GITHUB_API_TOKEN),
15 | }
16 |
17 | const github = (state = INITIAL_STATE, action) => {
18 | const { type, payload } = action
19 | switch (type) {
20 | case 'UPDATE_STATE':
21 | return {
22 | ...state,
23 | ...{[payload.state]: payload.data}
24 | }
25 | case 'UPDATE_STATS_FIELD':
26 | return Object.assign({}, state, {
27 | [payload.state] : {
28 | ...state[payload.state],
29 | ...payload.stats
30 | }
31 | })
32 | default:
33 | return state
34 | }
35 | }
36 |
37 | export default github
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "GitHub Stats",
3 | "name": "GitHub Statistics",
4 | "icons": [
5 | {
6 | "src": "github-sign-16.png",
7 | "type": "image/png",
8 | "sizes": "16x16"
9 | },
10 | {
11 | "src": "github-sign-24.png",
12 | "type": "image/png",
13 | "sizes": "24x24"
14 | },
15 | {
16 | "src": "github-sign-32.png",
17 | "type": "image/png",
18 | "sizes": "32x32"
19 | },
20 | {
21 | "src": "github-sign-64.png",
22 | "type": "image/png",
23 | "sizes": "64x64"
24 | },
25 | {
26 | "src": "github-sign-256.png",
27 | "type": "image/png",
28 | "sizes": "256x256"
29 | },
30 | {
31 | "src": "github-sign-512.png",
32 | "type": "image/png",
33 | "sizes": "512x512"
34 | }
35 | ],
36 | "start_url": ".",
37 | "display": "standalone",
38 | "theme_color": "#000000",
39 | "background_color": "#ffffff"
40 | }
41 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "env": {
4 | "browser": true,
5 | "es6": true
6 | },
7 | "extends": [
8 | "eslint:recommended",
9 | "plugin:react/recommended"
10 | ],
11 | "globals": {
12 | "Atomics": "readonly",
13 | "SharedArrayBuffer": "readonly"
14 | },
15 | "parserOptions": {
16 | "ecmaFeatures": {
17 | "jsx": true
18 | },
19 | "ecmaVersion": 2018,
20 | "sourceType": "module"
21 | },
22 | "plugins": [
23 | "react"
24 | ],
25 | "rules": {
26 | "indent": [
27 | "error",
28 | 2,
29 | {
30 | "SwitchCase": 1
31 | }
32 | ],
33 | "comma-dangle": 0, // disallow trailing commas in object literals
34 | "semi": [
35 | 1,
36 | "never"
37 | ],
38 | "react/jsx-filename-extension": [
39 | 1,
40 | {
41 | "extensions": [
42 | ".js",
43 | ".jsx"
44 | ]
45 | }
46 | ],
47 | "no-trailing-spaces": 1 // disallow trailing whitespace at the end of lines
48 | }
49 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "github-statistics",
3 | "homepage": "https://vesoft-inc.github.io/github-statistics",
4 | "version": "0.1.0",
5 | "private": true,
6 | "dependencies": {
7 | "@material-ui/core": "^4.11.4",
8 | "antd": "^3.26.20",
9 | "axios": "^0.26.1",
10 | "gh-pages": "^2.2.0",
11 | "graphql": "^14.7.0",
12 | "graphql-request": "^1.8.2",
13 | "highcharts": "^9.1.1",
14 | "highcharts-react-official": "^2.2.2",
15 | "lodash": "^4.17.21",
16 | "moment": "^2.29.1",
17 | "objects-to-csv": "^1.3.6",
18 | "react": "^16.14.0",
19 | "react-dom": "^16.14.0",
20 | "react-redux": "^7.2.4",
21 | "react-scripts": "3.1.1",
22 | "recharts": "^1.8.5",
23 | "redux": "^4.1.0"
24 | },
25 | "scripts": {
26 | "predeploy": "npm run build",
27 | "deploy": "gh-pages -d build",
28 | "start": "react-scripts start",
29 | "build": "react-scripts build",
30 | "test": "react-scripts test",
31 | "eject": "react-scripts eject",
32 | "export": "node csv"
33 | },
34 | "eslintConfig": {
35 | "extends": "react-app"
36 | },
37 | "browserslist": {
38 | "production": [
39 | ">0.2%",
40 | "not dead",
41 | "not op_mini all"
42 | ],
43 | "development": [
44 | "last 1 chrome version",
45 | "last 1 firefox version",
46 | "last 1 safari version"
47 | ]
48 | },
49 | "devDependencies": {
50 | "eslint": "^6.8.0",
51 | "eslint-plugin-react": "^7.24.0",
52 | "redux-devtools": "^3.7.0"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | GitHub Statistics
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ---
2 |
3 | GitHub Statistics
4 |
5 | ---
6 |
7 | 
8 | 
9 | 
10 | 
11 | 
12 |
13 | # Screenshots
14 |
15 | # Start
16 |
17 | 
18 |
19 | # Commit
20 |
21 | 
22 |
23 | # Features
24 |
25 | - [x] Repository overview
26 | - [x] Star history
27 | - [x] Fork history
28 | - [x] Commit history (recent year)
29 | - [x] Release assets (newest tag)
30 |
31 | # Contributions
32 |
33 | 
34 |
35 | If you have any suggestions of comments on either current codes or features, do not heisitate to create issues/PRs to let us know.
36 |
37 | Feature requests are also warmly welcomed.
38 |
39 | # Development
40 |
41 | 1. clone repo.
42 |
43 | ```shell
44 | git clone https://github.com/vesoft-inc/github-statistics.git
45 | ```
46 |
47 | 2. install npm modules.
48 |
49 | ```shell
50 | cd github-statistics
51 | npm install
52 | ```
53 |
54 | 3. **MUST SET** `YOUR_GITHUB_API_TOKEN` in the [.env](./.env) file. Read [Creating a personal access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens) to create token if you don't have one.
55 |
56 | 4. runs the app in the development mode, open [http://localhost:3000](http://localhost:3000) to view it in the browser.
57 |
58 | ```shell
59 | npm start
60 | ```
61 |
62 | Build the app for production to the `build` folder.
63 |
64 | ```shell
65 | npm run build
66 | ```
67 |
68 | ---
69 |
70 |
71 | # Made with ❤️ by vesoft-inc #
72 |
73 |
74 | ---
75 |
--------------------------------------------------------------------------------
/src/css/App.css:
--------------------------------------------------------------------------------
1 | @import '~antd/dist/antd.css';
2 |
3 | .header {
4 | position: fixed;
5 | width: 100%;
6 | background-color: #FFFFFF;
7 | z-index: 10;
8 | padding: 12px 2%;
9 | box-shadow: 0 2px 8px #f0f1f2;
10 | /* border-bottom: 1px solid #999999 */
11 | }
12 |
13 | .container {
14 | padding-top:60px;
15 | display: flex;
16 | flex-direction: row;
17 | margin: 0 2%;
18 | }
19 |
20 | .footer {
21 | width: 100%;
22 | height: auto;
23 | margin-top: 40px;
24 | background-color: rgba(28, 28, 30, 1);
25 | }
26 |
27 | .sider {
28 | }
29 |
30 | @media(max-width:600px){
31 | .sider {
32 | display: none;
33 | }
34 | }
35 |
36 | .content {
37 | /* text-align: center; */
38 | width: 100%;
39 | }
40 |
41 | /* Global reusable */
42 | .flex-center {
43 | display: flex;
44 | place-content: center;
45 | align-items: center;
46 | }
47 |
48 | .flex-center-left {
49 | display: flex;
50 | justify-content: flex-start;
51 | align-content: center;
52 | align-items: center;
53 | }
54 |
55 |
56 | /* Specific on top-level */
57 | .header-title {
58 | color: rgba(0, 0, 0, 0.85);
59 | font-weight: 600;
60 | font-size: 24px;
61 | }
62 |
63 | .header-title:hover {
64 | color: rgba(10, 132, 255, 1)
65 | }
66 |
67 | .header-section {
68 | margin: auto 12px;
69 | padding: 2px 0px;
70 | }
71 |
72 | .header-logo{
73 | position: absolute;
74 | right: 0;
75 | top: 0;
76 | width: 180px;
77 | height: 65px;
78 | z-index: 99;
79 | }
80 |
81 | .header-input {
82 | margin-right: 12px;
83 | width: 200px;
84 | }
85 |
86 | /* OVERLOADS */
87 | a:hover {
88 | color:rgba(0, 0, 0, 1);
89 | }
90 |
91 | .ant-anchor {
92 | font-size: 15px;
93 | padding: 20px 20px;
94 | }
95 |
96 | .ant-anchor-ink::before {
97 | position: relative;
98 | display: block;
99 | width: 0px;
100 | height: 100%;
101 | margin: 0 auto;
102 | content: '';
103 | }
104 |
105 | .ant-anchor-ink-ball {
106 | width: 1px;
107 | height: 8px;
108 | border: 1px solid #222;
109 | border-radius: 8px;
110 | }
111 |
112 | .ant-anchor-link-title.ant-anchor-link-title-active {
113 | color: rgba(0, 0, 0, 1);
114 | }
115 |
--------------------------------------------------------------------------------
/src/components/sections/Repository.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import moment from 'moment'
5 |
6 | import { Row, Statistic, Icon, Tag } from 'antd'
7 |
8 | import COLORS from './Colors'
9 |
10 | class Repository extends React.Component {
11 |
12 | _render = () => {
13 | const { stats, ready } = this.props
14 |
15 | return (
16 | <>
17 | {Array.from(stats.entries()).map((
18 | (pair, index) => {
19 | if (ready.get(pair[0])) {
20 | const { name, createdAt, primaryLanguage, pushedAt, watcherCount } = pair[1]
21 | const dateSinceCreated = Math.floor((Date.now() - new Date(createdAt).valueOf()) / (24*60*60*1000))
22 |
23 | return (
24 |
25 |
26 |
27 | {pair[0]}
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | } value={watcherCount} />
47 |
48 |
49 |
50 | )
51 | }
52 | return false
53 | }
54 | ))}
55 | >
56 | )
57 | }
58 |
59 | render() {
60 | return (
61 | <>
62 | {this._render()}
63 | >
64 | )
65 | }
66 | }
67 |
68 | Repository.propTypes = {
69 | id: PropTypes.string,
70 | repos: PropTypes.array,
71 | data: PropTypes.objectOf(Map),
72 | stats: PropTypes.objectOf(Map),
73 | ready: PropTypes.objectOf(Map),
74 | }
75 |
76 |
77 | export default Repository
--------------------------------------------------------------------------------
/src/components/DataUnit.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import _ from 'lodash'
4 |
5 | import { Card, Progress, Button, Row, Col, Icon, Divider } from 'antd'
6 |
7 | class DataUnit extends React.Component {
8 | constructor(props) {
9 | super(props)
10 |
11 | this.state = {
12 | loading: false,
13 | progress: 0,
14 | ready: false,
15 | }
16 |
17 | }
18 |
19 | _renderSection = (title, iconType = 'question', iconColor = 'black', action) => (
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | {title}
30 |
31 |
32 |
33 |
34 |
35 | {action}
36 |
37 |
38 |
39 | )
40 |
41 | _renderGetButton = action => {
42 | const { loading, progress } = this.state
43 |
44 | const updateProgress = progress => this.setState({ progress })
45 | const wrappedOnUpdate = param => {
46 | action.onUpdate(param)
47 | }
48 | const wrappedFinish = param => {
49 | this.setState({ loading: false, ready: true })
50 | action.onFinish(param)
51 | }
52 | const scriptCall = _.partial(action.type, wrappedOnUpdate, wrappedFinish, updateProgress)
53 | const wrappedOnClick = () => {
54 | this.setState({ loading: true, progress: 0 })
55 | scriptCall()
56 | }
57 | return (
58 |
59 |
62 |
63 |
64 | )
65 | }
66 |
67 | render() {
68 | const { ready } = this.state
69 | const {
70 | id,
71 | title,
72 | iconType,
73 | iconColor,
74 | children,
75 | action,
76 | } = this.props
77 | return (
78 |
79 | {this._renderSection(
80 | title, iconType, iconColor,
81 | this._renderGetButton(action)
82 | )}
83 | {ready && children}
84 |
85 |
86 | )
87 | }
88 | }
89 |
90 | DataUnit.propTypes = {
91 | id: PropTypes.string,
92 | children: PropTypes.any,
93 | title: PropTypes.string,
94 | iconColor: PropTypes.string,
95 | iconType: PropTypes.string,
96 | action: PropTypes.object,
97 | }
98 |
99 | export default DataUnit
--------------------------------------------------------------------------------
/csv.js:
--------------------------------------------------------------------------------
1 | const ObjectToCsv = require('objects-to-csv');
2 | const axios = require('axios');
3 |
4 | const token = 'token ' // Enter your Github token
5 |
6 | const repos = [
7 | {
8 | 'url': 'vesoft-inc/nebula', // repo github url
9 | 'name': 'nebula'
10 | },
11 | {
12 | 'url': 'esoft-inc/github-statistics',
13 | 'name': 'github-statistics'
14 | },
15 | ];
16 |
17 | let csv_datas = [];
18 | (async () => {
19 | let data_item = {};
20 | await Promise.all(
21 | repos.map(async _repo =>{
22 | try {
23 | const { data } = await axios.get(`https://api.github.com/repos/${_repo.url}`, {
24 | headers: {
25 | "Authorization": token
26 | }
27 | });
28 | // console.log("data",data)
29 | // commit
30 | const { headers:commit } = await axios.get(`https://api.github.com/repos/${data.full_name}/commits?per_page=1&page=1`, {
31 | headers: {
32 | "Authorization": token
33 | }
34 | });
35 |
36 | // issue
37 |
38 | const { headers: issue } = await axios.get(`https://api.github.com/repos/${data.full_name}/issues?state=closed&per_page=1&page=1`, {
39 | headers: {
40 | "Authorization": token
41 | }
42 | });
43 |
44 | // pull request count
45 |
46 | const {headers:pr_count_1} = await axios.get(`https://api.github.com/repos/${data.full_name}/pulls?state=open&per_page=1&page=1`, {
47 | headers: {
48 | "Authorization": token
49 | }
50 | });
51 | const {headers:pr_count_2} = await axios.get(`https://api.github.com/repos/${data.full_name}/pulls?state=closed&per_page=1&page=1`, {
52 | headers: {
53 | "Authorization": token
54 | }
55 | });
56 |
57 | data_item.repo_name = data.full_name.split("/")[0];
58 | data_item.date = data.created_at.slice(0, 10);
59 | data_item.fork_count = data.forks_count;
60 | data_item.star_count = data.stargazers_count;
61 | data_item.issue_count = issue.link.split(',')[1].split("page=")[2].split(">")[0] + data.open_issues_count;
62 | data_item.pr_count = Number(pr_count_1.link.split(',')[1].split("page=")[2].split(">")[0] ) + Number(pr_count_2.link.split(',')[1].split("page=")[2].split(">")[0]);
63 | data_item.commit_count = commit.link.split('page=')[4].split(">")[0];
64 | csv_datas = [...csv_datas, { ...data_item }];
65 | } catch (error) {
66 | console.log('error:', error)
67 | }
68 | })
69 | )
70 |
71 |
72 | const csv = new ObjectToCsv(csv_datas);
73 | // save to file
74 |
75 | await csv.toDisk('./repo_data.csv');
76 |
77 | // csv has exported
78 |
79 | console.log('csv has exported');
80 |
81 | })();
--------------------------------------------------------------------------------
/src/components/sections/Release.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import moment from 'moment'
5 |
6 | import { Row, Statistic, Icon, Tag, Table } from 'antd'
7 |
8 | import COLORS from './Colors'
9 |
10 | class Release extends React.Component {
11 |
12 | _render = () => {
13 | const { stats, data, ready } = this.props
14 |
15 | return (
16 | <>
17 | {Array.from(ready.entries()).map((
18 | (pair, index) => {
19 | if (pair[1]) { // ready
20 | const { totalAssets, name, tagName, createdAt, totalDownloads } = stats.get(pair[0])
21 |
22 | const dateSinceCreated = Math.floor((Date.now() - new Date(createdAt).valueOf()) / (24*60*60*1000))
23 | const averageDownloadsPerDay = totalDownloads / dateSinceCreated
24 |
25 | return (
26 |
27 |
28 |
29 | {pair[0]}
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | } />
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | )
54 | }
55 | return false
56 | }
57 | ))}
58 | >
59 | )
60 | }
61 |
62 | render() {
63 | return (
64 | <>
65 | {this._render()}
66 | >
67 | )
68 | }
69 | }
70 |
71 | const columns = [
72 | {
73 | title: 'Asset',
74 | dataIndex: 'name',
75 | key: 'name',
76 | },
77 | {
78 | title: 'Content type',
79 | dataIndex: 'contentType',
80 | key: 'contentType',
81 | },
82 | {
83 | title: 'Downloads',
84 | dataIndex: 'downloadCount',
85 | key: 'downloadCount',
86 | },
87 | {
88 | title: 'Created at',
89 | dataIndex: 'createdAt',
90 | key: 'createdAt',
91 | render: time => moment(time).format("MMMM Do YYYY, h:mm:ss a")
92 | },
93 | {
94 | title: 'Updated at',
95 | dataIndex: 'updatedAt',
96 | key: 'updatedAt',
97 | render: time => moment(time).format("MMMM Do YYYY, h:mm:ss a")
98 | },
99 |
100 | ]
101 |
102 |
103 | Release.propTypes = {
104 | id: PropTypes.string,
105 | repos: PropTypes.array,
106 | data: PropTypes.objectOf(Map),
107 | stats: PropTypes.objectOf(Map),
108 | ready: PropTypes.objectOf(Map),
109 | }
110 |
111 |
112 | export default Release
--------------------------------------------------------------------------------
/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.1/8 is considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl)
104 | .then(response => {
105 | // Ensure service worker exists, and that we really are getting a JS file.
106 | const contentType = response.headers.get('content-type');
107 | if (
108 | response.status === 404 ||
109 | (contentType != null && contentType.indexOf('javascript') === -1)
110 | ) {
111 | // No service worker found. Probably a different app. Reload the page.
112 | navigator.serviceWorker.ready.then(registration => {
113 | registration.unregister().then(() => {
114 | window.location.reload();
115 | });
116 | });
117 | } else {
118 | // Service worker found. Proceed as normal.
119 | registerValidSW(swUrl, config);
120 | }
121 | })
122 | .catch(() => {
123 | console.log(
124 | 'No internet connection found. App is running in offline mode.'
125 | );
126 | });
127 | }
128 |
129 | export function unregister() {
130 | if ('serviceWorker' in navigator) {
131 | navigator.serviceWorker.ready.then(registration => {
132 | registration.unregister();
133 | });
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/src/components/sections/Fork.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import { Row, Statistic, Icon, Tag } from 'antd'
5 | // import { LineChart, Line, CartesianGrid, XAxis, YAxis, ResponsiveContainer, Legend, Tooltip as ChartToolTip } from 'recharts'
6 | import Highcharts from 'highcharts'
7 | import HighchartsReact from 'highcharts-react-official'
8 |
9 | import COLORS from './Colors'
10 | import OPTIONS from './ChartOptions'
11 |
12 |
13 | class Fork extends React.Component {
14 | constructor(props) {
15 | super(props)
16 | this.state = {
17 | isReset: false,
18 | arr: []
19 | }
20 | }
21 | static formatter = (repo, data) => {
22 | // fprk total data, index 0
23 | let total = { name: repo, data: [] }
24 | // fork daily increment data, index 1
25 | let increment = { name: repo, data: [] }
26 |
27 | let cumulativeCount = 0
28 | data.forEach((value, key) => {
29 | cumulativeCount += value
30 | total.data.push([key, cumulativeCount])
31 | increment.data.push([key, value])
32 | })
33 |
34 | return [total, increment]
35 | }
36 |
37 | shouldComponentUpdate(nextProps) {
38 | return !nextProps.loading && !Array.from(nextProps.ready.values()).includes(false)
39 | }
40 |
41 | cloneMap(map) {
42 | let obj = Object.create(null)
43 | for (let [k, v] of map) {
44 | obj[k] = v
45 | }
46 | obj = JSON.parse(JSON.stringify(obj))
47 | let tmpMap = new Map()
48 | for (let k of Object.keys(obj)) {
49 | tmpMap.set(k, obj[k])
50 | }
51 | return tmpMap
52 | }
53 |
54 | componentWillReceiveProps(props) {
55 | this.setState({
56 | arr: this.cloneMap(props.data)
57 | })
58 | }
59 |
60 | resetData(min, max) {
61 | Array.from(this.state.arr.values()).map(dataArray => dataArray[0]).forEach((value, index) => {
62 | let initial = 0
63 | value.data.forEach((obj, index) => {
64 | if (min <= obj[0] && max >= obj[0]) {
65 | if (!initial) {
66 | initial = obj[1]
67 | value.data[index - 1] = 0
68 | }
69 | }
70 | if (obj) {
71 | obj[1] -= initial
72 | }
73 | })
74 | })
75 | this.setState({
76 | isReset: true
77 | })
78 | }
79 |
80 | _renderStatistics = () => {
81 | const { stats, ready } = this.props
82 |
83 | return (
84 | <>
85 | {Array.from(stats.entries()).map((
86 | (pair, index) => {
87 | if (ready.get(pair[0])) {
88 | const { total, maxIncrement, createdAt } = pair[1]
89 | const dateSinceCreated = Math.floor((Date.now() - new Date(createdAt).valueOf()) / (24*60*60*1000))
90 | const averagePerDay = total / dateSinceCreated
91 | return (
92 |
93 |
94 |
95 | {pair[0]}
96 |
97 |
98 |
99 | } />
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 | )
111 | }
112 | return false
113 | }
114 | ))}
115 | >
116 | )
117 | }
118 |
119 | _renderCharts = () => {
120 | const { data, ready } = this.props
121 |
122 | if (!Array.from(ready.values()).includes(true)) return
123 |
124 | return (
125 | <>
126 | {
132 | if (!event.resetSelection) {
133 | var min = event.xAxis[0].min;
134 | var max = event.xAxis[0].max;
135 | this.resetData(min, max)
136 | } else {
137 | this.setState({
138 | arr: this.cloneMap(data)
139 | })
140 | }
141 | }
142 | },
143 | zoomType: 'x',
144 | type: 'line'
145 | },
146 | xAxis: {
147 | type: 'datetime',
148 | },
149 | yAxis: {
150 | gridLineWidth: 0,
151 | title: {
152 | text: 'total forks',
153 | },
154 | },
155 | series: Array.from(this.state.arr.values()).map(dataArray => dataArray[0]),
156 | }}
157 | />
158 | dataArray[1]),
175 | }}
176 | />
177 | >
178 | )
179 | }
180 |
181 | render() {
182 | return (
183 | <>
184 | {this._renderStatistics()}
185 | {this._renderCharts()}
186 | >
187 | )
188 | }
189 | }
190 |
191 | Fork.propTypes = {
192 | id: PropTypes.string,
193 | repos: PropTypes.array,
194 | data: PropTypes.objectOf(Map),
195 | stats: PropTypes.objectOf(Map),
196 | ready: PropTypes.objectOf(Map),
197 | loading: PropTypes.bool,
198 | }
199 |
200 |
201 | export default Fork
--------------------------------------------------------------------------------
/src/components/sections/PullRequests.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import { Row, Statistic, Icon, Tag } from 'antd'
5 | // import { LineChart, Line, CartesianGrid, XAxis, YAxis, ResponsiveContainer, Legend, Tooltip as ChartToolTip } from 'recharts'
6 | import Highcharts from 'highcharts'
7 | import HighchartsReact from 'highcharts-react-official'
8 |
9 | import COLORS from './Colors'
10 | import OPTIONS from './ChartOptions'
11 |
12 |
13 | class Fork extends React.Component {
14 | constructor(props) {
15 | super(props)
16 | this.state = {
17 | isReset: false,
18 | arr: []
19 | }
20 | }
21 | static formatter = (repo, data) => {
22 | // fprk total data, index 0
23 | let total = { name: repo, data: [] }
24 | // fork daily increment data, index 1
25 | let increment = { name: repo, data: [] }
26 |
27 | let cumulativeCount = 0
28 | data.forEach((value, key) => {
29 | cumulativeCount += value
30 | total.data.push([key, cumulativeCount])
31 | increment.data.push([key, value])
32 | })
33 |
34 | return [total, increment]
35 | }
36 |
37 | shouldComponentUpdate(nextProps) {
38 | return !nextProps.loading && !Array.from(nextProps.ready.values()).includes(false)
39 | }
40 |
41 | cloneMap(map) {
42 | let obj = Object.create(null)
43 | for (let [k, v] of map) {
44 | obj[k] = v
45 | }
46 | obj = JSON.parse(JSON.stringify(obj))
47 | let tmpMap = new Map()
48 | for (let k of Object.keys(obj)) {
49 | tmpMap.set(k, obj[k])
50 | }
51 | return tmpMap
52 | }
53 |
54 | componentWillReceiveProps(props) {
55 | this.setState({
56 | arr: this.cloneMap(props.data)
57 | })
58 | }
59 |
60 | resetData(min, max) {
61 | Array.from(this.state.arr.values()).map(dataArray => dataArray[0]).forEach((value, index) => {
62 | let initial = 0
63 | value.data.forEach((obj, index) => {
64 | if (min <= obj[0] && max >= obj[0]) {
65 | if (!initial) {
66 | initial = obj[1]
67 | value.data[index - 1] = 0
68 | }
69 | }
70 | if (obj) {
71 | obj[1] -= initial
72 | }
73 | })
74 | })
75 | this.setState({
76 | isReset: true
77 | })
78 | }
79 |
80 | _renderStatistics = () => {
81 | const { stats, ready } = this.props
82 |
83 | return (
84 | <>
85 | {Array.from(stats.entries()).map((
86 | (pair, index) => {
87 | if (ready.get(pair[0])) {
88 | const { total, maxIncrement, createdAt } = pair[1]
89 | const dateSinceCreated = Math.floor((Date.now() - new Date(createdAt).valueOf()) / (24*60*60*1000))
90 | const averagePerDay = total / dateSinceCreated
91 | return (
92 |
93 |
94 |
95 | {pair[0]}
96 |
97 |
98 |
99 | } />
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 | )
111 | }
112 | return false
113 | }
114 | ))}
115 | >
116 | )
117 | }
118 |
119 | _renderCharts = () => {
120 | const { data, ready } = this.props
121 |
122 | if (!Array.from(ready.values()).includes(true)) return
123 |
124 | return (
125 | <>
126 | {
132 | if (!event.resetSelection) {
133 | var min = event.xAxis[0].min;
134 | var max = event.xAxis[0].max;
135 | this.resetData(min, max)
136 | } else {
137 | this.setState({
138 | arr: this.cloneMap(data)
139 | })
140 | }
141 | }
142 | },
143 | zoomType: 'x',
144 | type: 'line'
145 | },
146 | xAxis: {
147 | type: 'datetime',
148 | },
149 | yAxis: {
150 | gridLineWidth: 0,
151 | title: {
152 | text: 'total pull requests',
153 | },
154 | },
155 | series: Array.from(this.state.arr.values()).map(dataArray => dataArray[0]),
156 | }}
157 | />
158 | dataArray[1]),
175 | }}
176 | />
177 | >
178 | )
179 | }
180 |
181 | render() {
182 | return (
183 | <>
184 | {this._renderStatistics()}
185 | {this._renderCharts()}
186 | >
187 | )
188 | }
189 | }
190 |
191 | Fork.propTypes = {
192 | id: PropTypes.string,
193 | repos: PropTypes.array,
194 | data: PropTypes.objectOf(Map),
195 | stats: PropTypes.objectOf(Map),
196 | ready: PropTypes.objectOf(Map),
197 | loading: PropTypes.bool,
198 | }
199 |
200 |
201 | export default Fork
--------------------------------------------------------------------------------
/src/components/sections/Commit.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import { Row, Statistic, Icon, Tag } from 'antd'
5 | // import { LineChart, Line, CartesianGrid, XAxis, YAxis, ResponsiveContainer, Legend, Tooltip as ChartToolTip } from 'recharts'
6 | import Highcharts from 'highcharts'
7 | import HighchartsReact from 'highcharts-react-official'
8 |
9 | import COLORS from './Colors'
10 | import OPTIONS from './ChartOptions'
11 |
12 |
13 | class Commit extends React.Component {
14 | constructor(props) {
15 | super(props)
16 | this.state = {
17 | isReset: false,
18 | arr: []
19 | }
20 | }
21 | static formatter = (repo, data) => {
22 | // commit total data, index 0
23 | let total = { name: repo, data: [] }
24 | // commit daily increment data, index 1
25 | let increment = { name: repo, data: [] }
26 |
27 | let cumulativeCount = 0
28 |
29 | // traversal backwards
30 | Array.from(data.entries()).slice().reverse().forEach(
31 | pair => {
32 | cumulativeCount += pair[1]
33 | total.data.push([pair[0], cumulativeCount])
34 | increment.data.push([pair[0], pair[1]])
35 | }
36 | )
37 |
38 | return [total, increment]
39 | }
40 |
41 | shouldComponentUpdate(nextProps) {
42 | return !nextProps.loading && !Array.from(nextProps.ready.values()).includes(false)
43 | }
44 |
45 | cloneMap(map) {
46 | let obj = Object.create(null)
47 | for (let [k, v] of map) {
48 | obj[k] = v
49 | }
50 | obj = JSON.parse(JSON.stringify(obj))
51 | let tmpMap = new Map()
52 | for (let k of Object.keys(obj)) {
53 | tmpMap.set(k, obj[k])
54 | }
55 | return tmpMap
56 | }
57 |
58 | componentWillReceiveProps(props) {
59 | this.setState({
60 | arr: this.cloneMap(props.data)
61 | })
62 | }
63 |
64 | resetData(min, max) {
65 | Array.from(this.state.arr.values()).map(dataArray => dataArray[0]).forEach((value, index) => {
66 | let initial = 0
67 | value.data.forEach((obj, index) => {
68 | if (min <= obj[0] && max >= obj[0]) {
69 | if (!initial) {
70 | initial = obj[1]
71 | value.data[index - 1] = 0
72 | }
73 | }
74 | if (obj) {
75 | obj[1] -= initial
76 | }
77 | })
78 | })
79 | this.setState({
80 | isReset: true
81 | })
82 | }
83 |
84 | _renderStatistics = () => {
85 | const { stats, ready } = this.props
86 |
87 | return (
88 | <>
89 | {Array.from(stats.entries()).map((
90 | (pair, index) => {
91 | if (ready.get(pair[0])) {
92 | const { total, maxIncrement, createdAt } = pair[1]
93 | const dateSinceCreated = Math.floor((Date.now() - new Date(createdAt).valueOf()) / (24*60*60*1000))
94 | const averagePerDay = total / dateSinceCreated
95 | return (
96 |
97 |
98 |
99 | {pair[0]}
100 |
101 |
102 |
103 | } />
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 | )
115 | }
116 | return false
117 | }
118 | ))}
119 | >
120 | )
121 | }
122 |
123 | _renderCharts = () => {
124 | const { data, ready } = this.props
125 |
126 | if (!Array.from(ready.values()).includes(true)) return
127 |
128 | return (
129 | <>
130 | {
136 | if (!event.resetSelection) {
137 | var min = event.xAxis[0].min;
138 | var max = event.xAxis[0].max;
139 | this.resetData(min, max)
140 | } else {
141 | this.setState({
142 | arr: this.cloneMap(data)
143 | })
144 | }
145 | }
146 | },
147 | zoomType: 'x',
148 | type: 'line'
149 | },
150 | xAxis: {
151 | type: 'datetime',
152 | },
153 | yAxis: {
154 | gridLineWidth: 0,
155 | title: {
156 | text: 'total commits',
157 | },
158 | },
159 | series: Array.from(this.state.arr.values()).map(dataArray => dataArray[0]),
160 | }}
161 | />
162 | dataArray[1]),
179 | }}
180 | />
181 | >
182 | )
183 | }
184 |
185 | render() {
186 | return (
187 | <>
188 | {this._renderStatistics()}
189 | {this._renderCharts()}
190 | >
191 | )
192 | }
193 | }
194 |
195 | Commit.propTypes = {
196 | id: PropTypes.string,
197 | repos: PropTypes.array,
198 | data: PropTypes.objectOf(Map),
199 | stats: PropTypes.objectOf(Map),
200 | ready: PropTypes.objectOf(Map),
201 | loading: PropTypes.bool,
202 | }
203 |
204 |
205 | export default Commit
--------------------------------------------------------------------------------
/src/components/sections/Issues.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import { Row, Statistic, Icon, Tag } from 'antd'
5 | // import { LineChart, Line, CartesianGrid, XAxis, YAxis, ResponsiveContainer, Legend, Tooltip as ChartToolTip } from 'recharts'
6 | import Highcharts from 'highcharts'
7 | import HighchartsReact from 'highcharts-react-official'
8 |
9 | import COLORS from './Colors'
10 | import OPTIONS from './ChartOptions'
11 |
12 |
13 | class Fork extends React.Component {
14 | constructor(props) {
15 | super(props)
16 | this.state = {
17 | isReset: false,
18 | arr: []
19 | }
20 | }
21 |
22 | static formatter = (repo, data) => {
23 | // issues total data, index 0
24 | let total = { name: repo, data: [] }
25 | // issues daily increment data, index 1
26 | let increment = { name: repo, data: [] }
27 | let cumulativeCount = 0
28 | let arrayObj = Array.from(data);
29 | arrayObj.sort(function (a, b) {
30 | return a[0] - b[0];
31 | })
32 | arrayObj.forEach((value, key) => {
33 | cumulativeCount += value[1]
34 | total.data.push([value[0], cumulativeCount])
35 |
36 | increment.data.push([value[0], value])
37 | })
38 | return [total, increment]
39 | }
40 |
41 | shouldComponentUpdate(nextProps) {
42 | return !nextProps.loading && !Array.from(nextProps.ready.values()).includes(false)
43 | }
44 |
45 | cloneMap(map) {
46 | let obj = Object.create(null)
47 | for (let [k, v] of map) {
48 | obj[k] = v
49 | }
50 | obj = JSON.parse(JSON.stringify(obj))
51 | let tmpMap = new Map()
52 | for (let k of Object.keys(obj)) {
53 | tmpMap.set(k, obj[k])
54 | }
55 | return tmpMap
56 | }
57 |
58 | componentWillReceiveProps(props) {
59 | this.setState({
60 | arr: this.cloneMap(props.data)
61 | })
62 | }
63 |
64 | resetData(min, max) {
65 | Array.from(this.state.arr.values()).map(dataArray => dataArray[0]).forEach((value, index) => {
66 | let initial = 0
67 | value.data.forEach((obj, index) => {
68 | if (min <= obj[0] && max >= obj[0]) {
69 | if (!initial) {
70 | initial = obj[1]
71 | value.data[index - 1] = 0
72 | }
73 | }
74 | if (obj) {
75 | obj[1] -= initial
76 | }
77 | })
78 | })
79 | this.setState({
80 | isReset: true
81 | })
82 | }
83 |
84 | _renderStatistics = () => {
85 | const { stats, ready } = this.props
86 |
87 | return (
88 | <>
89 | {Array.from(stats.entries()).map((
90 | (pair, index) => {
91 | if (ready.get(pair[0])) {
92 | const { total, maxIncrement, createdAt } = pair[1]
93 | const dateSinceCreated = Math.floor((Date.now() - new Date(createdAt).valueOf()) / (24*60*60*1000))
94 | const averagePerDay = total / dateSinceCreated
95 | return (
96 |
97 |
98 |
99 | {pair[0]}
100 |
101 |
102 |
103 | } />
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 | )
115 | }
116 | return false
117 | }
118 | ))}
119 | >
120 | )
121 | }
122 |
123 | _renderCharts = () => {
124 | const { data, ready } = this.props
125 |
126 | if (!Array.from(ready.values()).includes(true)) return
127 |
128 | return (
129 | <>
130 | {
136 | if (!event.resetSelection) {
137 | var min = event.xAxis[0].min;
138 | var max = event.xAxis[0].max;
139 | this.resetData(min, max)
140 | } else {
141 | this.setState({
142 | arr: this.cloneMap(data)
143 | })
144 | }
145 | }
146 | },
147 | zoomType: 'x',
148 | type: 'line'
149 | },
150 | xAxis: {
151 | type: 'datetime',
152 | },
153 | yAxis: {
154 | gridLineWidth: 0,
155 | title: {
156 | text: 'total issues',
157 | },
158 | },
159 | series: Array.from(this.state.arr.values()).map(dataArray => dataArray[0]),
160 | }}
161 | />
162 | dataArray[1]),
179 | }}
180 | />
181 | >
182 | )
183 | }
184 |
185 | render() {
186 | return (
187 | <>
188 | {this._renderStatistics()}
189 | {this._renderCharts()}
190 | >
191 | )
192 | }
193 | }
194 |
195 | Fork.propTypes = {
196 | id: PropTypes.string,
197 | repos: PropTypes.array,
198 | data: PropTypes.objectOf(Map),
199 | stats: PropTypes.objectOf(Map),
200 | ready: PropTypes.objectOf(Map),
201 | loading: PropTypes.bool,
202 | }
203 |
204 |
205 | export default Fork
--------------------------------------------------------------------------------
/src/css/normalize.css:
--------------------------------------------------------------------------------
1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
2 |
3 | /* Document
4 | ========================================================================== */
5 |
6 | /**
7 | * 1. Correct the line height in all browsers.
8 | * 2. Prevent adjustments of font size after orientation changes in iOS.
9 | */
10 |
11 | html {
12 | line-height: 1.15; /* 1 */
13 | -webkit-text-size-adjust: 100%; /* 2 */
14 | }
15 |
16 | /* Sections
17 | ========================================================================== */
18 |
19 | /**
20 | * Remove the margin in all browsers.
21 | */
22 |
23 | body {
24 | margin: 0;
25 | }
26 |
27 | /**
28 | * Render the `main` element consistently in IE.
29 | */
30 |
31 | main {
32 | display: block;
33 | }
34 |
35 | /**
36 | * Correct the font size and margin on `h1` elements within `section` and
37 | * `article` contexts in Chrome, Firefox, and Safari.
38 | */
39 |
40 | h1 {
41 | font-size: 2em;
42 | margin: 0.67em 0;
43 | }
44 |
45 | /* Grouping content
46 | ========================================================================== */
47 |
48 | /**
49 | * 1. Add the correct box sizing in Firefox.
50 | * 2. Show the overflow in Edge and IE.
51 | */
52 |
53 | hr {
54 | box-sizing: content-box; /* 1 */
55 | height: 0; /* 1 */
56 | overflow: visible; /* 2 */
57 | }
58 |
59 | /**
60 | * 1. Correct the inheritance and scaling of font size in all browsers.
61 | * 2. Correct the odd `em` font sizing in all browsers.
62 | */
63 |
64 | pre {
65 | font-family: monospace, monospace; /* 1 */
66 | font-size: 1em; /* 2 */
67 | }
68 |
69 | /* Text-level semantics
70 | ========================================================================== */
71 |
72 | /**
73 | * Remove the gray background on active links in IE 10.
74 | */
75 |
76 | a {
77 | background-color: transparent;
78 | }
79 |
80 | /**
81 | * 1. Remove the bottom border in Chrome 57-
82 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
83 | */
84 |
85 | abbr[title] {
86 | border-bottom: none; /* 1 */
87 | text-decoration: underline; /* 2 */
88 | text-decoration: underline dotted; /* 2 */
89 | }
90 |
91 | /**
92 | * Add the correct font weight in Chrome, Edge, and Safari.
93 | */
94 |
95 | b,
96 | strong {
97 | font-weight: bolder;
98 | }
99 |
100 | /**
101 | * 1. Correct the inheritance and scaling of font size in all browsers.
102 | * 2. Correct the odd `em` font sizing in all browsers.
103 | */
104 |
105 | code,
106 | kbd,
107 | samp {
108 | font-family: monospace, monospace; /* 1 */
109 | font-size: 1em; /* 2 */
110 | }
111 |
112 | /**
113 | * Add the correct font size in all browsers.
114 | */
115 |
116 | small {
117 | font-size: 80%;
118 | }
119 |
120 | /**
121 | * Prevent `sub` and `sup` elements from affecting the line height in
122 | * all browsers.
123 | */
124 |
125 | sub,
126 | sup {
127 | font-size: 75%;
128 | line-height: 0;
129 | position: relative;
130 | vertical-align: baseline;
131 | }
132 |
133 | sub {
134 | bottom: -0.25em;
135 | }
136 |
137 | sup {
138 | top: -0.5em;
139 | }
140 |
141 | /* Embedded content
142 | ========================================================================== */
143 |
144 | /**
145 | * Remove the border on images inside links in IE 10.
146 | */
147 |
148 | img {
149 | border-style: none;
150 | }
151 |
152 | /* Forms
153 | ========================================================================== */
154 |
155 | /**
156 | * 1. Change the font styles in all browsers.
157 | * 2. Remove the margin in Firefox and Safari.
158 | */
159 |
160 | button,
161 | input,
162 | optgroup,
163 | select,
164 | textarea {
165 | font-family: inherit; /* 1 */
166 | font-size: 100%; /* 1 */
167 | line-height: 1.15; /* 1 */
168 | margin: 0; /* 2 */
169 | }
170 |
171 | /**
172 | * Show the overflow in IE.
173 | * 1. Show the overflow in Edge.
174 | */
175 |
176 | button,
177 | input { /* 1 */
178 | overflow: visible;
179 | }
180 |
181 | /**
182 | * Remove the inheritance of text transform in Edge, Firefox, and IE.
183 | * 1. Remove the inheritance of text transform in Firefox.
184 | */
185 |
186 | button,
187 | select { /* 1 */
188 | text-transform: none;
189 | }
190 |
191 | /**
192 | * Correct the inability to style clickable types in iOS and Safari.
193 | */
194 |
195 | button,
196 | [type="button"],
197 | [type="reset"],
198 | [type="submit"] {
199 | -webkit-appearance: button;
200 | }
201 |
202 | /**
203 | * Remove the inner border and padding in Firefox.
204 | */
205 |
206 | button::-moz-focus-inner,
207 | [type="button"]::-moz-focus-inner,
208 | [type="reset"]::-moz-focus-inner,
209 | [type="submit"]::-moz-focus-inner {
210 | border-style: none;
211 | padding: 0;
212 | }
213 |
214 | /**
215 | * Restore the focus styles unset by the previous rule.
216 | */
217 |
218 | button:-moz-focusring,
219 | [type="button"]:-moz-focusring,
220 | [type="reset"]:-moz-focusring,
221 | [type="submit"]:-moz-focusring {
222 | outline: 1px dotted ButtonText;
223 | }
224 |
225 | /**
226 | * Correct the padding in Firefox.
227 | */
228 |
229 | fieldset {
230 | padding: 0.35em 0.75em 0.625em;
231 | }
232 |
233 | /**
234 | * 1. Correct the text wrapping in Edge and IE.
235 | * 2. Correct the color inheritance from `fieldset` elements in IE.
236 | * 3. Remove the padding so developers are not caught out when they zero out
237 | * `fieldset` elements in all browsers.
238 | */
239 |
240 | legend {
241 | box-sizing: border-box; /* 1 */
242 | color: inherit; /* 2 */
243 | display: table; /* 1 */
244 | max-width: 100%; /* 1 */
245 | padding: 0; /* 3 */
246 | white-space: normal; /* 1 */
247 | }
248 |
249 | /**
250 | * Add the correct vertical alignment in Chrome, Firefox, and Opera.
251 | */
252 |
253 | progress {
254 | vertical-align: baseline;
255 | }
256 |
257 | /**
258 | * Remove the default vertical scrollbar in IE 10+.
259 | */
260 |
261 | textarea {
262 | overflow: auto;
263 | }
264 |
265 | /**
266 | * 1. Add the correct box sizing in IE 10.
267 | * 2. Remove the padding in IE 10.
268 | */
269 |
270 | [type="checkbox"],
271 | [type="radio"] {
272 | box-sizing: border-box; /* 1 */
273 | padding: 0; /* 2 */
274 | }
275 |
276 | /**
277 | * Correct the cursor style of increment and decrement buttons in Chrome.
278 | */
279 |
280 | [type="number"]::-webkit-inner-spin-button,
281 | [type="number"]::-webkit-outer-spin-button {
282 | height: auto;
283 | }
284 |
285 | /**
286 | * 1. Correct the odd appearance in Chrome and Safari.
287 | * 2. Correct the outline style in Safari.
288 | */
289 |
290 | [type="search"] {
291 | -webkit-appearance: textfield; /* 1 */
292 | outline-offset: -2px; /* 2 */
293 | }
294 |
295 | /**
296 | * Remove the inner padding in Chrome and Safari on macOS.
297 | */
298 |
299 | [type="search"]::-webkit-search-decoration {
300 | -webkit-appearance: none;
301 | }
302 |
303 | /**
304 | * 1. Correct the inability to style clickable types in iOS and Safari.
305 | * 2. Change font properties to `inherit` in Safari.
306 | */
307 |
308 | ::-webkit-file-upload-button {
309 | -webkit-appearance: button; /* 1 */
310 | font: inherit; /* 2 */
311 | }
312 |
313 | /* Interactive
314 | ========================================================================== */
315 |
316 | /*
317 | * Add the correct display in Edge, IE 10+, and Firefox.
318 | */
319 |
320 | details {
321 | display: block;
322 | }
323 |
324 | /*
325 | * Add the correct display in all browsers.
326 | */
327 |
328 | summary {
329 | display: list-item;
330 | }
331 |
332 | /* Misc
333 | ========================================================================== */
334 |
335 | /**
336 | * Add the correct display in IE 10+.
337 | */
338 |
339 | template {
340 | display: none;
341 | }
342 |
343 | /**
344 | * Add the correct display in IE 10.
345 | */
346 |
347 | [hidden] {
348 | display: none;
349 | }
--------------------------------------------------------------------------------
/src/components/sections/Star.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | import { Row, Statistic, Icon, Tag } from 'antd'
5 | // import { LineChart, Line, CartesianGrid, XAxis, YAxis, ResponsiveContainer, Legend, Tooltip as ChartToolTip } from 'recharts'
6 | import Highcharts from 'highcharts'
7 | import HighchartsReact from 'highcharts-react-official'
8 |
9 | import COLORS from './Colors'
10 | import OPTIONS from './ChartOptions'
11 |
12 |
13 | class Star extends React.Component {
14 | constructor(props) {
15 | super(props)
16 | this.state = {
17 | isReset: false,
18 | arr: []
19 | }
20 | }
21 | static formatter = (repo, data) => {
22 | // star total data, index 0
23 | let total = { name: repo, data: [] }
24 | // star daily increment data, index 1
25 | let increment = { name: repo, data: [] }
26 |
27 | let cumulativeCount = 0
28 | data.forEach((value, key) => {
29 | cumulativeCount += value
30 | total.data.push([key, cumulativeCount])
31 | increment.data.push([key, value])
32 | })
33 |
34 | return [total, increment]
35 | }
36 |
37 | shouldComponentUpdate(nextProps) {
38 | return !nextProps.loading && !Array.from(nextProps.ready.values()).includes(false)
39 | }
40 | cloneMap(map) {
41 | let obj = Object.create(null);
42 | for (let [k, v] of map) {
43 | obj[k] = v;
44 | }
45 | obj = JSON.parse(JSON.stringify(obj));
46 | let tmpMap = new Map();
47 | for (let k of Object.keys(obj)) {
48 | tmpMap.set(k, obj[k]);
49 | }
50 | return tmpMap;
51 | }
52 | componentWillReceiveProps(props) {
53 | this.setState({
54 | arr: this.cloneMap(props.data)
55 | })
56 | }
57 |
58 | resetData(min, max) {
59 | Array.from(this.state.arr.values()).map(dataArray => dataArray[0]).forEach((value, index) => {
60 | let initial = 0
61 | value.data.forEach((obj, index) => {
62 | if (min <= obj[0] && max >= obj[0]) {
63 | if (!initial) {
64 | initial = obj[1]
65 | value.data[index-1] = 0
66 | }
67 | }
68 | if (obj) {
69 | obj[1] -= initial
70 | }
71 | })
72 | })
73 | this.setState({
74 | isReset: true
75 | })
76 | }
77 | _renderStatistics = () => {
78 | const { stats, ready } = this.props
79 |
80 | return (
81 | <>
82 | {Array.from(stats.entries()).map((
83 | (pair, index) => {
84 | if (ready.get(pair[0])) {
85 | const { total, maxIncrement, createdAt } = pair[1]
86 | const dateSinceCreated = Math.floor((Date.now() - new Date(createdAt).valueOf()) / (24 * 60 * 60 * 1000))
87 | const averagePerDay = total / dateSinceCreated
88 | return (
89 |
90 |
91 |
92 | {pair[0]}
93 |
94 |
95 |
96 | } />
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 | )
108 | }
109 | return false
110 | }
111 | ))}
112 | >
113 | )
114 | }
115 |
116 | _renderCharts = () => {
117 | const { data, ready } = this.props
118 | if (!Array.from(ready.values()).includes(true)) return
119 | return (
120 | <>
121 | {
128 | if (!event.resetSelection) {
129 | var min = event.xAxis[0].min;
130 | var max = event.xAxis[0].max;
131 | this.resetData(min, max)
132 | } else {
133 | this.setState({
134 | arr: this.cloneMap(data)
135 | })
136 | }
137 | }
138 | },
139 | zoomType: 'x',
140 | type: 'line'
141 | },
142 | xAxis: {
143 | type: 'datetime',
144 | },
145 | yAxis: {
146 | gridLineWidth: 0,
147 | title: {
148 | text: 'total stars',
149 | },
150 | },
151 | series: Array.from(this.state.arr.values()).map(dataArray => dataArray[0]),
152 | }}
153 | />
154 | dataArray[1]),
172 | }}
173 | />
174 | >
175 | )
176 | }
177 |
178 | // _renderLines = (dataIndex) => {
179 | // const { data, ready } = this.props
180 | // const dataReady = Array.from(ready.values())
181 | // console.log("Lines are rendered")
182 | // return Array.from(data.values()).map((dataArray, index) => (
183 | // dataReady[index]
184 | // ?
185 | //
194 | // :
195 | // <>>
196 | // ))
197 | // }
198 |
199 | // _renderCharts = () => {
200 | // const { ready } = this.props
201 |
202 | // if (!Array.from(ready.values()).includes(true)) return
203 |
204 | // return (
205 | // <>
206 | //
207 | //
208 | //
209 | //
210 | //
211 | //
212 | // new Date(ms).toISOString().slice(0,10)}
219 | // />
220 | //
221 | // new Date(ms).toISOString().slice(0,10)}/>
222 | // {this._renderLines(0)}
223 | //
224 | //
225 | //
226 | //
227 | //
228 | //
229 | //
230 | //
231 | // new Date(ms).toISOString().slice(0,10)}
238 | // />
239 | //
240 | // new Date(ms).toISOString().slice(0,10)}/>
241 | // {this._renderLines(1)}
242 | //
243 | //
244 | //
245 | //
246 | // >
247 | // )
248 | // }
249 |
250 | render() {
251 | return (
252 | <>
253 | {this._renderStatistics()}
254 | {this._renderCharts()}
255 | >
256 | )
257 | }
258 | }
259 |
260 | Star.propTypes = {
261 | id: PropTypes.string,
262 | repos: PropTypes.array,
263 | data: PropTypes.objectOf(Map),
264 | stats: PropTypes.objectOf(Map),
265 | ready: PropTypes.objectOf(Map),
266 | loading: PropTypes.bool,
267 | }
268 |
269 |
270 | export default Star
--------------------------------------------------------------------------------
/src/components/GithubStatistics.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { connect } from 'react-redux'
3 | import PropTypes from 'prop-types'
4 | import _ from 'lodash'
5 |
6 | import TYPES from './DataTypes'
7 | import COLORS from './sections/Colors'
8 |
9 | import { Row, Col, Anchor, Button, Tag, Tooltip, message, Select } from 'antd'
10 |
11 | import DataSection from './DataSection'
12 |
13 | import GithubFetcher from '../scripts/GithubFetcher'
14 |
15 | import { updateState } from '../actions'
16 | import logo from '../image/logo.png'
17 | // const CENTER_FLEX = { display: 'flex', placeContent: 'center' }
18 | // const CENTER_LEFT_FLEX = { display: 'flex', justifyContent: 'flex-start', alignContent: 'center'}
19 |
20 | // message.config({
21 | // top: 60,
22 | // duration: 2,
23 | // maxCount: 5,
24 | // })
25 | /* eslint-disable-next-line */
26 | const GITHUB_API_TOKEN = process.env.REACT_APP_GITHUB_API_TOKEN
27 |
28 | class GithubStatistics extends React.Component {
29 | constructor(props) {
30 | super(props)
31 |
32 | this.state = {
33 | repos:[],
34 | input: undefined,
35 | suggestions: [],
36 | testingRepo: false,
37 | deleteRepo: '',
38 | }
39 |
40 |
41 | /* eslint-disable-next-line */
42 | var t = btoa(GITHUB_API_TOKEN)
43 | /* eslint-disable-next-line */
44 | this.fetcher = new GithubFetcher(t)
45 | this.props.updateState("githubApiToken", t)
46 |
47 | this.search = _.debounce(
48 | this.fetcher.searchRepository,
49 | 300,
50 | { leading: false, trailing: true }
51 | ).bind(this)
52 | }
53 |
54 | componentDidMount() {
55 | try {
56 | if (JSON.parse(localStorage.getItem("repos"))) {
57 | this.setState({
58 | repos: JSON.parse(localStorage.getItem("repos")),
59 | deleteRepo: '',
60 | })
61 | }
62 | } catch (error) {
63 | console.log(error)
64 | }
65 | }
66 |
67 | deleteRepo = index => {
68 | const { repos } = this.state
69 | const deleteRepo = repos[index]
70 | repos.splice(index, 1)
71 | this.setState({
72 | repos: [...repos],
73 | deleteRepo: deleteRepo,
74 | }, () => {
75 | localStorage.setItem("repos", JSON.stringify([...repos]))
76 | })
77 | }
78 |
79 | addRepo = repo => {
80 | const { repos } = this.state
81 | if (repos.includes(repo)) {
82 | message.error(`${repo} is already added`)
83 | }else {
84 | this.setState({
85 | repos: [ ...repos, repo],
86 | deleteRepo: '',
87 | }, () => {
88 | localStorage.setItem("repos", JSON.stringify([...repos, repo]))
89 | })
90 | }
91 | }
92 |
93 | // _handleAdding = repo => {
94 | // const slashIndex = repo.indexOf('/')
95 | // const owner = repo.slice(0, slashIndex)
96 | // const name = repo.slice(slashIndex + 1)
97 |
98 | // this.setState({ testingRepo: true })
99 | // this.fetcher.testRepository(owner, name,
100 | // result => {
101 | // this.setState({ testingRepo: false })
102 | // if (result) {
103 | // this.addRepo(repo)
104 | // message.success(repo + ' added')
105 | // } else {
106 | // message.error('Repository not found')
107 | // }
108 | // }
109 | // )
110 | // }
111 |
112 | _renderTags = () => {
113 | const { repos } = this.state
114 |
115 | return (
116 | repos.map((repo, index) => (
117 | this.deleteRepo(index)}>
118 | {repo}
119 |
120 | ))
121 | )
122 | }
123 |
124 | _renderHeaderInput = () => {
125 | const { repos, input, testingRepo, suggestions } = this.state
126 |
127 | // const format = /^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}\/{1}[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}$/i
128 | let hintMessage = ''
129 |
130 | // Conditions
131 | const inputEmpty = input === undefined
132 | // const formatIncorrect = !format.test(input)
133 | const repoExisted = repos.includes(input)
134 |
135 | if (repoExisted) hintMessage = 'Repository already added'
136 | // if (formatIncorrect) hintMessage = 'Input incorrectly formatted'
137 | if (inputEmpty) hintMessage = 'Empty'
138 |
139 | const disabled = inputEmpty || repoExisted
140 |
141 | return (
142 |
143 | {/* }
146 | placeholder="owner/name"
147 | value={input}
148 | onChange={e => {
149 | this.setState({ input: e.target.value })
150 | this.fetcher.searchRepository(e.target.value, suggestions => this.setState({ suggestions }))
151 | }}
152 | onPressEnter={() => !disabled && this._handleAdding(input)}
153 | disabled={testingRepo}
154 | allowClear
155 | /> */}
156 |
175 |
178 |
186 |
187 | )
188 | }
189 |
190 | render() {
191 | // const dotStyle = {strokeWidth: 2, r: 2.5}
192 | const { repos, deleteRepo } = this.state
193 |
194 | return (
195 |
196 |
215 |
216 |
217 |
218 | {Object.values(TYPES).map(value => (
219 |
220 | ))}
221 |
222 |
223 |
224 |
229 |
230 |
235 |
236 |
241 |
242 |
247 |
248 |
253 |
258 |
263 |
264 |
265 |
266 |
268 |
269 | )
270 | }
271 | }
272 |
273 | GithubStatistics.propTypes = {
274 | updateState: PropTypes.func,
275 | }
276 |
277 | const mapDispatchToProps = dispatch => ({
278 | updateState: (state, data) => dispatch(updateState(state, data)),
279 | })
280 |
281 | export default connect(
282 | null,
283 | mapDispatchToProps,
284 | )(GithubStatistics)
--------------------------------------------------------------------------------
/src/components/DataSection.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { connect } from 'react-redux'
4 | // import _ from 'lodash'
5 |
6 | import TYPES from './DataTypes'
7 | import '../css/DataSection.css'
8 |
9 | import { Progress, Button, Row, Icon, Tag, Popover } from 'antd'
10 | import GithubFetcher from '../scripts/GithubFetcher'
11 |
12 | import Repository from './sections/Repository'
13 | import Star from './sections/Star'
14 | import Fork from './sections/Fork'
15 | import Commit from './sections/Commit'
16 | import Release from './sections/Release'
17 | import Issues from './sections/Issues'
18 | import PullRequests from './sections/PullRequests'
19 |
20 | class DataSection extends React.Component {
21 | constructor(props) {
22 | super(props)
23 |
24 | this.state = {
25 | progress: new Map(),
26 | data: new Map(),
27 | stats: new Map(),
28 | visible: new Map(),
29 | ready: new Map(),
30 | loading: false,
31 | }
32 |
33 | const { githubApiToken, type } = this.props
34 |
35 | this.fetcher = new GithubFetcher(githubApiToken)
36 |
37 | switch (type) {
38 | case TYPES.REPO:
39 | this.icon =
40 | this.body = Repository
41 | this.fetchCall = this.fetcher.fetchRepositoryData
42 | break
43 | case TYPES.STAR:
44 | this.info = 'Star trend data are huge, they might take minutes to load.'
45 | this.icon =
46 | this.body = Star
47 | this.fetchCall = this.fetcher.fetchStargazerData
48 | break
49 | case TYPES.FORK:
50 | this.icon =
51 | this.body = Fork
52 | this.fetchCall = this.fetcher.fetchForkData
53 | break
54 | case TYPES.COMMIT:
55 | this.info = 'Because of the API restriction, only commits in a recent year will be loaded.'
56 | this.icon =
57 | this.body = Commit
58 | this.fetchCall = this.fetcher.fetchCommitData
59 | break
60 | case TYPES.RELEASE:
61 | this.icon =
62 | this.body = Release
63 | this.fetchCall = this.fetcher.fetchReleaseData
64 | break
65 | case TYPES.ISSUES:
66 | this.icon =
67 | this.body = Issues
68 | this.fetchCall = this.fetcher.fetchIssuesData
69 | break
70 | case TYPES.PULLREQUESTS:
71 | this.icon =
72 | this.body = PullRequests
73 | this.fetchCall = this.fetcher.fetchRequestsData
74 | break
75 | default:
76 | console.log('TYPE DOESNOT EXIST')
77 | return 'ERROR'
78 | }
79 |
80 | // data formatter
81 | this.formatter = this.body.formatter
82 | }
83 |
84 |
85 | componentDidUpdate(prevProps) {
86 | const { deleteRepo, repos } = this.props
87 | const { data, stats, progress, visible, loading, ready } = this.state
88 |
89 | // delete repo out
90 | if (deleteRepo !== prevProps.deleteRepo && deleteRepo !== '') {
91 | data.delete(deleteRepo)
92 | stats.delete(deleteRepo)
93 | progress.delete(deleteRepo)
94 | ready.delete(deleteRepo)
95 | visible.delete(deleteRepo)
96 | this.setState({ data, stats, progress, ready, visible, loading: this._getAllProgress() !== 100 && loading && repos.length !== 0 })
97 | }
98 |
99 | // new repo in
100 | if (prevProps.repos !== repos && deleteRepo === '') {
101 | const newRepo = repos.filter(repo => !prevProps.repos.includes(repo))
102 | newRepo.forEach(repo => {
103 | data.set(repo, {})
104 | stats.set(repo, {})
105 | progress.set(repo, 0)
106 | ready.set(repo, false)
107 | visible.set(repo, true)
108 | this.setState({ data, stats, progress, ready, visible })
109 | if (loading) {
110 | this._fetch(repo)
111 | }
112 | })
113 | }
114 | }
115 |
116 | /**
117 | * fetching from a specific repository
118 | * for a specific data type from DataTypes.js
119 | * @param repo repo to fectch
120 | * @returns exit status string
121 | */
122 | _fetch = repo => {
123 | const { repos } = this.props
124 | const slashIndex = repo.indexOf('/')
125 | const owner = repo.slice(0, slashIndex)
126 | const name = repo.slice(slashIndex + 1)
127 |
128 | const onUpdate = data => {
129 | if(this.state.data.has(repo)) {
130 | this.state.data.set(
131 | repo,
132 | this.formatter ? this.formatter(repo, data) : data,
133 | )
134 | this.setState({ data: this.state.data })
135 | }
136 | }
137 | const onFinish = stats => {
138 | if(this.state.stats.has(repo)) {
139 | this.state.stats.set(repo, stats)
140 | this.state.ready.set(repo, true)
141 | this.setState({ stats: this.state.stats, ready: this.state.ready})
142 | }
143 | if (this._getAllProgress() === 100) {
144 | this.setState({ loading: false })
145 | }
146 | }
147 | const onProgress = progress => {
148 | if(this.state.progress.has(repo)) {
149 | this.state.progress.set(repo,progress)
150 | this.setState({
151 | progress:this.state.progress
152 | })
153 | }
154 | }
155 | const shouldAbort = () => {
156 | // if (this._getAllProgress() === 100) {
157 | // this.setState({ loading: false })
158 | // }
159 | return !repos.includes(repo)
160 | }
161 |
162 | this.fetchCall(
163 | owner, name,
164 | onUpdate,
165 | onFinish,
166 | onProgress,
167 | shouldAbort,
168 | )
169 |
170 | return 'FETCH REQUESTED'
171 | }
172 |
173 | /**
174 | * get progress of fetching all
175 | * @returns progress as number from 0 to 100
176 | */
177 | _getAllProgress = () => {
178 | const { progress } = this.state
179 | return Math.floor(Array.from(progress.values()).reduce((a, b) => a + b, 0)/ (progress.size === 0 ? 1 : progress.size))
180 | }
181 |
182 | _renderUpdateAllButton = () => {
183 | const { loading, ready } = this.state
184 | const { repos } = this.props
185 |
186 | return (
187 |
213 | )
214 | }
215 |
216 | _renderRepoTags = () => {
217 | const { progress, visible } = this.state
218 | const { repos } = this.props
219 |
220 | return (
221 | repos.map(repo => (
222 |
223 |
230 |
{
234 | visible.set(repo, checked)
235 | this.setState({ visible })
236 | }}
237 | >
238 | {repo}
239 |
240 |
241 | ))
242 | )
243 | }
244 |
245 | _renderBody = () => {
246 | const { data, stats, ready, loading } = this.state
247 | const { repos } = this.props
248 |
249 | return
250 | }
251 |
252 | render() {
253 | const { type } = this.props
254 |
255 | return (
256 |
257 |
258 |
259 | {this.icon}
260 |
261 | {type}
262 |
263 | {this.info ?
264 |
265 |
266 | : null }
267 |
268 |
269 | {this._renderRepoTags()}
270 |
271 |
277 |
278 | {this._renderUpdateAllButton()}
279 |
280 |
281 | {this._renderBody()}
282 |
283 | )
284 | }
285 |
286 | }
287 |
288 | DataSection.propTypes = {
289 | githubApiToken: PropTypes.string,
290 | repos: PropTypes.array,
291 | deleteRepo: PropTypes.string,
292 | type: PropTypes.string,
293 | }
294 |
295 | const mapStateToProps = state => ({
296 | githubApiToken: state.github.githubApiToken
297 | })
298 |
299 | export default connect(
300 | mapStateToProps,
301 | )(DataSection)
--------------------------------------------------------------------------------
/src/scripts/GithubFetcher.js:
--------------------------------------------------------------------------------
1 | import { } from 'graphql'
2 | import { GraphQLClient } from 'graphql-request'
3 |
4 | const getProgress = (c, t) => t === 0 ? 100 : Math.floor(c / t * 100)
5 |
6 | class GithubFetcher {
7 |
8 | constructor(token) {
9 | const endpoint = 'https://api.github.com/graphql'
10 |
11 | this.gqlClient = new GraphQLClient(
12 | endpoint,
13 | {
14 | headers: {
15 | Authorization: 'bearer ' + window.atob(token),
16 | }
17 | }
18 | )
19 |
20 | // configurations
21 | this.liveUpdate = false
22 | this.pagesPerUpdate = 20
23 | }
24 |
25 | /**
26 | * test if the repository exists
27 | * @param owner owner of the repository
28 | * @param name of the repository
29 | * @param onResult (@param result) function that will be called when test finishes
30 | * @return false if not exist, true otherwise
31 | */
32 | // testRepository = async (owner, name, onResult) => {
33 | // const variables = {
34 | // owner: owner,
35 | // name: name,
36 | // }
37 |
38 | // const query = /* GraphQL */ `
39 | // query getRepository($owner: String!, $name: String!){
40 | // repository(owner: $owner, name: $name) {
41 | // id
42 | // }
43 | // }
44 | // `
45 |
46 | // try {
47 | // await this.gqlClient.request(query, variables)
48 | // } catch (error) {
49 | // if (onResult) {
50 | // onResult(false)
51 | // }
52 | // return false
53 | // }
54 |
55 | // if (onResult) onResult(true)
56 | // return true
57 | // }
58 |
59 | /**
60 | * suggest possible repositories based on current input
61 | * @param onResult (@param result) function that will be called when search finishes
62 | */
63 | searchRepository = async (input, onResult) => {
64 | const variables = {
65 | query: input,
66 | }
67 |
68 | const query = /* GraphQL */ `
69 | query searchRepository($query: String!){
70 | search(query: $query, first: 5, type: REPOSITORY) {
71 | codeCount
72 | nodes {
73 | ...on Repository {
74 | nameWithOwner
75 | }
76 | }
77 | }
78 | }
79 | `
80 | let formattedData = []
81 |
82 | const data = await this.gqlClient.request(query, variables)
83 |
84 | data.search.nodes.forEach(repo => formattedData.push(repo.nameWithOwner))
85 |
86 | if (onResult) onResult(formattedData)
87 |
88 | return formattedData
89 | }
90 |
91 | /**
92 | * fetch repository low-level data
93 | * @param owner owner of the repository
94 | * @param name name of the repository
95 | * @param onUpdate (data) function that will be called when a new data update is avaiable
96 | * @param onFinish (stats) function that will be called when fetching is finished
97 | * @param onProgress (progress) function that will be called when progress is updated
98 | * @param shouldAbort function that returns a boolean which determines whether fetching should abort
99 | * @returns Object that contains statistics
100 | */
101 | fetchRepositoryData = async (owner, name, onUpdate, onFinish, onProgress, shouldAbort) => {
102 | const variables = {
103 | owner: owner,
104 | name: name,
105 | }
106 |
107 | // define the graphql query
108 | const query = /* GraphQL */ `
109 | query getRepository($owner: String!, $name: String!){
110 | repository(owner: $owner, name: $name) {
111 | nameWithOwner
112 | createdAt
113 | primaryLanguage {
114 | name
115 | }
116 | pushedAt
117 | watchers(first: 0) {
118 | totalCount
119 | }
120 | }
121 | }
122 | `
123 |
124 | // update progress tracking
125 | if (onProgress) onProgress(10)
126 |
127 | const data = await this.gqlClient.request(query, variables)
128 | // if (shouldAbort) {
129 | // if (shouldAbort()) {
130 | // return
131 | // }
132 | // }
133 |
134 | const formattedData = {
135 | name: data.repository.nameWithOwner,
136 | createdAt: data.repository.createdAt,
137 | primaryLanguage: data.repository.primaryLanguage.name,
138 | pushedAt: data.repository.pushedAt,
139 | watcherCount: data.repository.watchers.totalCount,
140 | }
141 |
142 | // update progress tracking
143 | if (onProgress) onProgress(100)
144 |
145 | if (onFinish) onFinish(formattedData)
146 |
147 | return formattedData
148 | }
149 |
150 | /**
151 | * fetch repository low-level data
152 | * @param owner owner of the repository
153 | * @param name name of the repository
154 | * @param onUpdate (data) function that will be called when a new data update is avaiable
155 | * @param onFinish (stats) function that will be called when fetching is finished
156 | * @param onProgress (progress) function that will be called when progress is updated
157 | * @param shouldAbort function that returns a boolean which determines whether fetching should abort
158 | * @returns Object that contains statistics
159 | */
160 | fetchStargazerData = async (owner, name, onUpdate = () => {}, onFinish, onProgress, shouldAbort) => {
161 | const preparationVariables = {
162 | owner: owner,
163 | name: name,
164 | }
165 |
166 | // define the graphql query
167 | const preparationQuery = /* GraphQL */ `
168 | query prepareStargazers($owner: String!, $name: String!){
169 | repository(owner: $owner, name: $name) {
170 | createdAt
171 | stargazers(first: 100) {
172 | totalCount
173 | }
174 | }
175 | }
176 | `
177 | const query = /* GraphQL */ `
178 | query getStargazers($owner: String!, $name: String!, $previousEndCursor: String){
179 | repository(owner: $owner, name: $name) {
180 | stargazers(first: 100, after: $previousEndCursor) {
181 | pageInfo {
182 | endCursor
183 | hasNextPage
184 | }
185 | edges {
186 | starredAt
187 | }
188 | }
189 | }
190 | }
191 | `
192 |
193 | // local variables
194 | const formattedData = new Map()
195 | let pageIndex = 0
196 | let totalToFetch = 0
197 | let maxIncrement = 0
198 | let numberFetched = 0
199 | let previousEndCursor = null
200 | let hasNextPage = false
201 |
202 | // Preparation query
203 | const preparationData = await this.gqlClient.request(preparationQuery, preparationVariables)
204 |
205 | // from preparation
206 | totalToFetch = preparationData.repository.stargazers.totalCount
207 | const createdAt = preparationData.repository.createdAt
208 |
209 | const handleEdge = edge => {
210 | const date = new Date(edge.starredAt.slice(0,10)).getTime() // ISO-8601 encoded UTC date string
211 | if (!formattedData.has(date)) {
212 | formattedData.set(date, 1)
213 | } else {
214 | formattedData.set(date, formattedData.get(date) + 1)
215 | }
216 | if (formattedData.get(date) > maxIncrement) maxIncrement = formattedData.get(date)
217 | // update progress tracking
218 | numberFetched += 1
219 | }
220 |
221 | // data traversal, 100 edges/request
222 | do {
223 | if (shouldAbort) if (shouldAbort()) return
224 |
225 | const variables = {
226 | owner: owner,
227 | name: name,
228 | previousEndCursor: previousEndCursor
229 | }
230 | // query for data
231 | const data = await new Promise(resolve => {
232 | const _data = this.gqlClient.request(query, variables)
233 | setTimeout(() => resolve(_data), 255)
234 | })
235 |
236 | data.repository.stargazers.edges.forEach(handleEdge)
237 |
238 | // update progress tracking
239 | if (onProgress) onProgress(getProgress(numberFetched, totalToFetch))
240 |
241 | // track loop-level variables
242 | previousEndCursor = data.repository.stargazers.pageInfo.endCursor
243 | hasNextPage = data.repository.stargazers.pageInfo.hasNextPage
244 | // update pageIndex
245 | pageIndex += 1
246 | // onUpdate callback if existed
247 | if (this.liveUpdate && onUpdate && pageIndex % this.pagesPerUpdate === 0) {
248 | onUpdate(formattedData)
249 | }
250 | } while (hasNextPage)
251 | if (onUpdate) onUpdate(formattedData)
252 | if (onFinish) onFinish({
253 | total: totalToFetch,
254 | maxIncrement,
255 | createdAt,
256 | })
257 | return formattedData
258 | }
259 |
260 | /**
261 | * fetch fork data
262 | * @param owner owner of the repository
263 | * @param name name of the repository
264 | * @param onUpdate (data) function that will be called when a new data update is avaiable
265 | * @param onFinish (stats) function that will be called when fetching is finished
266 | * @param onProgress (progress) function that will be called when progress is updated
267 | * @param shouldAbort function that returns a boolean which determines whether fetching should abort
268 | * @returns Object that contains statistics
269 | */
270 | fetchForkData = async (owner, name, onUpdate, onFinish, onProgress, shouldAbort) => {
271 | const preparationVariables = {
272 | owner: owner,
273 | name: name,
274 | }
275 |
276 | // define the graphql query
277 | const preparationQuery = /* GraphQL */ `
278 | query prepareForks($owner: String!, $name: String!){
279 | repository(owner: $owner, name: $name) {
280 | createdAt
281 | forkCount
282 | forks(first: 0) {
283 | totalCount
284 | }
285 | }
286 | }
287 | `
288 | const query = /* GraphQL */ `
289 | query getForks($owner: String!, $name: String!, $previousEndCursor: String){
290 | repository(owner: $owner, name: $name) {
291 | forks(first: 100, after: $previousEndCursor) {
292 | pageInfo {
293 | endCursor
294 | hasNextPage
295 | }
296 | nodes {
297 | createdAt
298 | }
299 | }
300 | }
301 | }
302 | `
303 |
304 | // local variables
305 | const formattedData = new Map()
306 | let pageIndex = 0
307 | let totalToFetch = 0
308 | let maxIncrement = 0
309 | let numberFetched = 0
310 | let previousEndCursor = null
311 | let hasNextPage = false
312 |
313 | // Preparation query
314 | const preparationData = await this.gqlClient.request(preparationQuery, preparationVariables)
315 |
316 | // from preparation
317 | totalToFetch = preparationData.repository.forks.totalCount
318 | const createdAt = preparationData.repository.createdAt
319 |
320 |
321 |
322 | const handleNode = node => {
323 | const date = new Date(node.createdAt.slice(0,10)).getTime() // ISO-8601 encoded UTC date string
324 | if (!formattedData.has(date)) {
325 | formattedData.set(date, 1)
326 | } else {
327 | formattedData.set(date, formattedData.get(date) + 1)
328 | }
329 | if (formattedData.get(date) > maxIncrement) maxIncrement = formattedData.get(date)
330 | // update progress tracking
331 | numberFetched += 1
332 | }
333 |
334 | // data traversal, 100 edges/request
335 | do {
336 | if (shouldAbort) if (shouldAbort()) return
337 |
338 | const variables = {
339 | owner: owner,
340 | name: name,
341 | previousEndCursor: previousEndCursor
342 | }
343 | // query for data
344 | const data = await this.gqlClient.request(query, variables)
345 |
346 | data.repository.forks.nodes.forEach(handleNode)
347 |
348 | // update progress tracking
349 | if (onProgress) onProgress(getProgress(numberFetched, totalToFetch))
350 |
351 | // track loop-level variables
352 | previousEndCursor = data.repository.forks.pageInfo.endCursor
353 | hasNextPage = data.repository.forks.pageInfo.hasNextPage
354 |
355 | // update pageIndex
356 | pageIndex += 1
357 |
358 | // onUpdate callback if existed
359 | if (this.liveUpdate && onUpdate && pageIndex % this.pagesPerUpdate === 0) {
360 | onUpdate(formattedData)
361 | }
362 | } while (hasNextPage)
363 |
364 | if (onUpdate) onUpdate(formattedData)
365 | if (onFinish) onFinish({
366 | total: totalToFetch,
367 | maxIncrement,
368 | createdAt,
369 | })
370 |
371 | return formattedData
372 | }
373 |
374 | /**
375 | * fetch repository low-level data
376 | * @param owner owner of the repository
377 | * @param name name of the repository
378 | * @param onUpdate (data) function that will be called when a new data update is avaiable
379 | * @param onFinish (stats) function that will be called when fetching is finished
380 | * @param onProgress (progress) function that will be called when progress is updated
381 | * @param shouldAbort function that returns a boolean which determines whether fetching should abort
382 | * @returns Object that contains statistics
383 | */
384 | fetchRequestsData = async (owner, name, onUpdate, onFinish, onProgress, shouldAbort) => {
385 | const preparationVariables = {
386 | owner: owner,
387 | name: name,
388 | }
389 |
390 | // define the graphql query
391 | const preparationQuery = /* GraphQL */ `
392 | query prepareForks($owner: String!, $name: String!){
393 | repository(owner: $owner, name: $name) {
394 | createdAt
395 | forkCount
396 | pullRequests(first: 0) {
397 | totalCount
398 | }
399 | }
400 | }
401 | `
402 | const query = /* GraphQL */ `
403 | query getForks($owner: String!, $name: String!, $previousEndCursor: String){
404 | repository(owner: $owner, name: $name) {
405 | pullRequests(first: 100, after: $previousEndCursor) {
406 | pageInfo {
407 | endCursor
408 | hasNextPage
409 | }
410 | nodes {
411 | createdAt
412 | }
413 | }
414 | }
415 | }
416 | `
417 |
418 | // local variables
419 | const formattedData = new Map()
420 | let pageIndex = 0
421 | let totalToFetch = 0
422 | let maxIncrement = 0
423 | let numberFetched = 0
424 | let previousEndCursor = null
425 | let hasNextPage = false
426 |
427 | // Preparation query
428 | const preparationData = await this.gqlClient.request(preparationQuery, preparationVariables)
429 |
430 | // from preparation
431 | totalToFetch = preparationData.repository.pullRequests.totalCount
432 | const createdAt = preparationData.repository.createdAt
433 |
434 |
435 |
436 | const handleNode = node => {
437 | const date = new Date(node.createdAt.slice(0, 10)).getTime() // ISO-8601 encoded UTC date string
438 | if (!formattedData.has(date)) {
439 | formattedData.set(date, 1)
440 | } else {
441 | formattedData.set(date, formattedData.get(date) + 1)
442 | }
443 | if (formattedData.get(date) > maxIncrement) maxIncrement = formattedData.get(date)
444 | // update progress tracking
445 | numberFetched += 1
446 | }
447 |
448 | // data traversal, 100 edges/request
449 | do {
450 | if (shouldAbort) if (shouldAbort()) return
451 |
452 | const variables = {
453 | owner: owner,
454 | name: name,
455 | previousEndCursor: previousEndCursor
456 | }
457 | // query for data
458 | const data = await this.gqlClient.request(query, variables)
459 |
460 | data.repository.pullRequests.nodes.forEach(handleNode)
461 |
462 | // update progress tracking
463 | if (onProgress) onProgress(getProgress(numberFetched, totalToFetch))
464 |
465 | // track loop-level variables
466 | previousEndCursor = data.repository.pullRequests.pageInfo.endCursor
467 | hasNextPage = data.repository.pullRequests.pageInfo.hasNextPage
468 |
469 | // update pageIndex
470 | pageIndex += 1
471 |
472 | // onUpdate callback if existed
473 | if (this.liveUpdate && onUpdate && pageIndex % this.pagesPerUpdate === 0) {
474 | onUpdate(formattedData)
475 | }
476 | } while (hasNextPage)
477 |
478 | if (onUpdate) onUpdate(formattedData)
479 | if (onFinish) onFinish({
480 | total: totalToFetch,
481 | maxIncrement,
482 | createdAt,
483 | })
484 |
485 | return formattedData
486 | }
487 |
488 | /**
489 | * fetch repository low-level data
490 | * @param owner owner of the repository
491 | * @param name name of the repository
492 | * @param onUpdate (data) function that will be called when a new data update is avaiable
493 | * @param onFinish (stats) function that will be called when fetching is finished
494 | * @param onProgress (progress) function that will be called when progress is updated
495 | * @param shouldAbort function that returns a boolean which determines whether fetching should abort
496 | * @returns Object that contains statistics
497 | */
498 | fetchIssuesData = async (owner, name, onUpdate, onFinish, onProgress, shouldAbort) => {
499 | const preparationVariables = {
500 | owner: owner,
501 | name: name,
502 | }
503 |
504 | // define the graphql query
505 | const preparationQuery = /* GraphQL */ `
506 | query prepareForks($owner: String!, $name: String!){
507 | repository(owner: $owner, name: $name) {
508 | createdAt
509 | forkCount
510 | issues(first: 0) {
511 | totalCount
512 | }
513 | }
514 | }
515 | `
516 | const query = /* GraphQL */ `
517 | query getForks($owner: String!, $name: String!, $previousEndCursor: String){
518 | repository(owner: $owner, name: $name) {
519 | issues(first: 100, after: $previousEndCursor) {
520 | pageInfo {
521 | endCursor
522 | hasNextPage
523 | }
524 | nodes {
525 | createdAt
526 | }
527 | }
528 | }
529 | }
530 | `
531 |
532 | // local variables
533 | const formattedData = new Map()
534 | let pageIndex = 0
535 | let totalToFetch = 0
536 | let maxIncrement = 0
537 | let numberFetched = 0
538 | let previousEndCursor = null
539 | let hasNextPage = false
540 |
541 | // Preparation query
542 | const preparationData = await this.gqlClient.request(preparationQuery, preparationVariables)
543 |
544 | // from preparation
545 | totalToFetch = preparationData.repository.issues.totalCount
546 | const createdAt = preparationData.repository.createdAt
547 |
548 |
549 |
550 | const handleNode = node => {
551 | const date = new Date(node.createdAt.slice(0, 10)).getTime() // ISO-8601 encoded UTC date string
552 | if (!formattedData.has(date)) {
553 | formattedData.set(date, 1)
554 | } else {
555 | formattedData.set(date, formattedData.get(date) + 1)
556 | }
557 | if (formattedData.get(date) > maxIncrement) maxIncrement = formattedData.get(date)
558 | // update progress tracking
559 | numberFetched += 1
560 | }
561 |
562 | // data traversal, 100 edges/request
563 | do {
564 | if (shouldAbort) if (shouldAbort()) return
565 |
566 | const variables = {
567 | owner: owner,
568 | name: name,
569 | previousEndCursor: previousEndCursor
570 | }
571 | // query for data
572 | const data = await this.gqlClient.request(query, variables)
573 |
574 | data.repository.issues.nodes.forEach(handleNode)
575 |
576 | // update progress tracking
577 | if (onProgress) onProgress(getProgress(numberFetched, totalToFetch))
578 |
579 | // track loop-level variables
580 | previousEndCursor = data.repository.issues.pageInfo.endCursor
581 | hasNextPage = data.repository.issues.pageInfo.hasNextPage
582 |
583 | // update pageIndex
584 | pageIndex += 1
585 |
586 | // onUpdate callback if existed
587 | if (this.liveUpdate && onUpdate && pageIndex % this.pagesPerUpdate === 0) {
588 | onUpdate(formattedData)
589 | }
590 | } while (hasNextPage)
591 |
592 | if (onUpdate) onUpdate(formattedData)
593 | if (onFinish) onFinish({
594 | total: totalToFetch,
595 | maxIncrement,
596 | createdAt,
597 | })
598 |
599 | return formattedData
600 | }
601 |
602 | /**
603 | * fetch repository low-level data
604 | * @param owner owner of the repository
605 | * @param name name of the repository
606 | * @param onUpdate (data) function that will be called when a new data update is avaiable
607 | * @param onFinish (stats) function that will be called when fetching is finished
608 | * @param onProgress (progress) function that will be called when progress is updated
609 | * @param shouldAbort function that returns a boolean which determines whether fetching should abort
610 | * @returns Object that contains statistics
611 | */
612 | fetchCommitData = async (owner, name, onUpdate, onFinish, onProgress, shouldAbort) => {
613 | const preparationVariables = {
614 | owner: owner,
615 | name: name,
616 | }
617 |
618 | // define the graphql query
619 | const preparationQuery = /* GraphQL */ `
620 | query prepareCommits($owner: String!, $name: String!) {
621 | repository(owner: $owner, name: $name) {
622 | defaultBranchRef {
623 | # name
624 | target {
625 | ... on Commit {
626 | oid
627 | committedDate
628 | history {
629 | totalCount
630 | }
631 | }
632 | }
633 | }
634 | }
635 | }
636 | `
637 | const query = /* GraphQL */ `
638 | query getCommits($owner: String!, $name: String!, $previousEndCursor: String, $oid: GitObjectID!, $since: GitTimestamp!){
639 | repository(owner: $owner, name: $name) {
640 | object(oid: $oid) {
641 | ... on Commit {
642 | history(first: 100, after: $previousEndCursor, since: $since ) {
643 | totalCount
644 | pageInfo {
645 | endCursor
646 | hasNextPage
647 | }
648 | nodes {
649 | committedDate
650 | # message
651 | }
652 | }
653 | }
654 | }
655 | }
656 | }
657 | `
658 |
659 |
660 | // local variables
661 | const formattedData = new Map()
662 | let pageIndex = 0
663 | let totalToFetch = 0
664 | let numberFetched = 0
665 | let maxIncrement = 0
666 | let previousEndCursor = null
667 | let hasNextPage = false
668 |
669 | // Preparation query
670 | const preparationData = await this.gqlClient.request(preparationQuery, preparationVariables)
671 |
672 | // from preparation
673 | totalToFetch = preparationData.repository.defaultBranchRef.target.history.totalCount
674 | const headRefOid = preparationData.repository.defaultBranchRef.target.oid
675 | const since = new Date(new Date(preparationData.repository.defaultBranchRef.target.committedDate)
676 | .setFullYear(new Date(preparationData.repository.defaultBranchRef.target.committedDate).getFullYear() - 1))
677 | .toISOString()
678 |
679 | const handleNode = node => {
680 | const date = new Date(node.committedDate.slice(0,10)).getTime() // ISO-8601 encoded UTC date string
681 | if (!formattedData.has(date)) {
682 | formattedData.set(date, 1)
683 | } else {
684 | formattedData.set(date, formattedData.get(date) + 1)
685 | }
686 | if (formattedData.get(date) > maxIncrement) maxIncrement = formattedData.get(date)
687 | // update progress tracking
688 | numberFetched += 1
689 | }
690 |
691 | // data traversal, 100 edges/request
692 | do {
693 | if (shouldAbort) if (shouldAbort()) return
694 |
695 | const variables = {
696 | owner: owner,
697 | name: name,
698 | oid: headRefOid,
699 | since: since,
700 | previousEndCursor: previousEndCursor
701 | }
702 | // query for data
703 | const data = await this.gqlClient.request(query, variables)
704 |
705 | totalToFetch = data.repository.object.history.totalCount
706 | data.repository.object.history.nodes.forEach(handleNode)
707 |
708 | // update progress tracking
709 | if (onProgress) onProgress(getProgress(numberFetched, totalToFetch))
710 |
711 | // track loop-level variables
712 | previousEndCursor = data.repository.object.history.pageInfo.endCursor
713 | hasNextPage = data.repository.object.history.pageInfo.hasNextPage
714 | // update pageIndex
715 | pageIndex += 1
716 |
717 | // onUpdate callback if existed
718 | if (this.liveUpdate && onUpdate && pageIndex % this.pagesPerUpdate === 0) {
719 | onUpdate(formattedData)
720 | }
721 | } while (hasNextPage)
722 |
723 | if (onUpdate) onUpdate(formattedData)
724 | if (onFinish) onFinish({
725 | total: totalToFetch,
726 | maxIncrement,
727 | createdAt: since,
728 | })
729 |
730 | return formattedData
731 | }
732 |
733 | /**
734 | * fetch release data
735 | * @param owner owner of the repository
736 | * @param name name of the repository
737 | * @param onUpdate (data) function that will be called when a new data update is avaiable
738 | * @param onFinish (stats) function that will be called when fetching is finished
739 | * @param onProgress (progress) function that will be called when progress is updated
740 | * @param shouldAbort function that returns a boolean which determines whether fetching should abort
741 | * @returns Object that contains statistics
742 | */
743 | fetchReleaseData = async (owner, name, onUpdate, onFinish, onProgress, shouldAbort) => {
744 | const variables = {
745 | owner: owner,
746 | name: name,
747 | }
748 |
749 | // define the graphql query
750 | const query = /* GraphQL */ `
751 | query getRelease($owner: String!, $name: String!){
752 | repository(owner: $owner, name: $name) {
753 | releases(first: 1, orderBy:{field:CREATED_AT,direction: DESC}) {
754 | totalCount
755 | nodes {
756 | name
757 | tagName
758 | createdAt
759 | releaseAssets (first: 20) {
760 | totalCount
761 | nodes {
762 | id
763 | name
764 | updatedAt
765 | contentType
766 | createdAt
767 | downloadCount
768 |
769 | }
770 | }
771 | }
772 | }
773 | }
774 | }
775 | `
776 |
777 | // local variables
778 | const formattedData = []
779 | let totalToFetch = 0
780 | let numberFetched = 0
781 | let totalDownloads = 0
782 |
783 | // Preparation query
784 | const data = await this.gqlClient.request(query, variables)
785 | // if (shouldAbort) {
786 | // if (shouldAbort()) {
787 | // return
788 | // }
789 | // }
790 |
791 | if (data.repository.releases.totalCount !== 0) {
792 | // from preparation
793 | totalToFetch = data.repository.releases.nodes[0].releaseAssets.totalCount
794 |
795 | // get stats of each asset
796 | data.repository.releases.nodes[0].releaseAssets.nodes.forEach(asset => {
797 | formattedData.push({
798 | id: asset.id,
799 | name: asset.name,
800 | updatedAt: asset.updatedAt,
801 | contentType: asset.contentType,
802 | createdAt: asset.createdAt,
803 | downloadCount: asset.downloadCount,
804 | })
805 |
806 | totalDownloads += asset.downloadCount
807 |
808 | numberFetched += 1
809 | if (onProgress) onProgress(getProgress(numberFetched, totalToFetch))
810 | })
811 |
812 | if (onProgress) onProgress(100)
813 |
814 | if (onUpdate) onUpdate(formattedData)
815 |
816 | if (onFinish) onFinish({
817 | totalAssets: totalToFetch,
818 | totalDownloads: totalDownloads,
819 | name: data.repository.releases.nodes[0].name,
820 | tagName: data.repository.releases.nodes[0].tagName,
821 | createdAt: data.repository.releases.nodes[0].createdAt
822 | })
823 | } else {
824 | if (onProgress) onProgress(100)
825 |
826 | if (onUpdate) onUpdate(formattedData)
827 |
828 | if (onFinish) onFinish({
829 | totalAssets: totalToFetch,
830 | totalDownloads: totalDownloads,
831 | })
832 | }
833 |
834 | return formattedData
835 | }
836 | }
837 |
838 | export default GithubFetcher
--------------------------------------------------------------------------------