summaryrefslogtreecommitdiff
path: root/app/assets/javascripts/contributors
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2019-10-28 18:06:15 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2019-10-28 18:06:15 +0000
commit7515ec41c527c62bfd56f46e388cf6d9fe06479f (patch)
tree614b555ec428b7eac4b836473d43516c41f9da46 /app/assets/javascripts/contributors
parenta77db6bc47d8cdd9edae2ec22f640821d0794404 (diff)
downloadgitlab-ce-7515ec41c527c62bfd56f46e388cf6d9fe06479f.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app/assets/javascripts/contributors')
-rw-r--r--app/assets/javascripts/contributors/components/contributors.vue227
-rw-r--r--app/assets/javascripts/contributors/index.js23
-rw-r--r--app/assets/javascripts/contributors/services/contributors_service.js7
-rw-r--r--app/assets/javascripts/contributors/stores/actions.js20
-rw-r--r--app/assets/javascripts/contributors/stores/getters.js33
-rw-r--r--app/assets/javascripts/contributors/stores/index.js18
-rw-r--r--app/assets/javascripts/contributors/stores/mutation_types.js3
-rw-r--r--app/assets/javascripts/contributors/stores/mutations.js17
-rw-r--r--app/assets/javascripts/contributors/stores/state.js5
-rw-r--r--app/assets/javascripts/contributors/utils.js30
10 files changed, 383 insertions, 0 deletions
diff --git a/app/assets/javascripts/contributors/components/contributors.vue b/app/assets/javascripts/contributors/components/contributors.vue
new file mode 100644
index 00000000000..7dd6b051cb4
--- /dev/null
+++ b/app/assets/javascripts/contributors/components/contributors.vue
@@ -0,0 +1,227 @@
+<script>
+import { __ } from '~/locale';
+import _ from 'underscore';
+import { mapActions, mapState, mapGetters } from 'vuex';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { GlAreaChart } from '@gitlab/ui/dist/charts';
+import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
+import { getDatesInRange } from '~/lib/utils/datetime_utility';
+import { xAxisLabelFormatter, dateFormatter } from '../utils';
+
+export default {
+ components: {
+ GlAreaChart,
+ GlLoadingIcon,
+ },
+ props: {
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ branch: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ masterChart: null,
+ individualCharts: [],
+ svgs: {},
+ masterChartHeight: 264,
+ individualChartHeight: 216,
+ };
+ },
+ computed: {
+ ...mapState(['chartData', 'loading']),
+ ...mapGetters(['showChart', 'parsedData']),
+ masterChartData() {
+ const data = {};
+ this.xAxisRange.forEach(date => {
+ data[date] = this.parsedData.total[date] || 0;
+ });
+ return [
+ {
+ name: __('Commits'),
+ data: Object.entries(data),
+ },
+ ];
+ },
+ masterChartOptions() {
+ return {
+ ...this.getCommonChartOptions(true),
+ yAxis: {
+ name: __('Number of commits'),
+ },
+ grid: {
+ bottom: 64,
+ left: 64,
+ right: 20,
+ top: 20,
+ },
+ };
+ },
+ individualChartsData() {
+ const maxNumberOfIndividualContributorsCharts = 100;
+
+ return Object.keys(this.parsedData.byAuthor)
+ .map(name => {
+ const author = this.parsedData.byAuthor[name];
+ return {
+ name,
+ email: author.email,
+ commits: author.commits,
+ dates: [
+ {
+ name: __('Commits'),
+ data: this.xAxisRange.map(date => [date, author.dates[date] || 0]),
+ },
+ ],
+ };
+ })
+ .sort((a, b) => b.commits - a.commits)
+ .slice(0, maxNumberOfIndividualContributorsCharts);
+ },
+ individualChartOptions() {
+ return {
+ ...this.getCommonChartOptions(false),
+ yAxis: {
+ name: __('Commits'),
+ max: this.individualChartYAxisMax,
+ },
+ grid: {
+ bottom: 27,
+ left: 64,
+ right: 20,
+ top: 8,
+ },
+ };
+ },
+ individualChartYAxisMax() {
+ return this.individualChartsData.reduce((acc, item) => {
+ const values = item.dates[0].data.map(value => value[1]);
+ return Math.max(acc, ...values);
+ }, 0);
+ },
+ xAxisRange() {
+ const dates = Object.keys(this.parsedData.total).sort((a, b) => new Date(a) - new Date(b));
+
+ const firstContributionDate = new Date(dates[0]);
+ const lastContributionDate = new Date(dates[dates.length - 1]);
+
+ return getDatesInRange(firstContributionDate, lastContributionDate, dateFormatter);
+ },
+ firstContributionDate() {
+ return this.xAxisRange[0];
+ },
+ lastContributionDate() {
+ return this.xAxisRange[this.xAxisRange.length - 1];
+ },
+ charts() {
+ return _.uniq(this.individualCharts);
+ },
+ },
+ mounted() {
+ this.fetchChartData(this.endpoint);
+ },
+ methods: {
+ ...mapActions(['fetchChartData']),
+ getCommonChartOptions(isMasterChart) {
+ return {
+ xAxis: {
+ type: 'time',
+ name: '',
+ data: this.xAxisRange,
+ axisLabel: {
+ formatter: xAxisLabelFormatter,
+ showMaxLabel: false,
+ showMinLabel: false,
+ },
+ boundaryGap: false,
+ splitNumber: isMasterChart ? 24 : 18,
+ // 28 days
+ minInterval: 28 * 86400 * 1000,
+ min: this.firstContributionDate,
+ max: this.lastContributionDate,
+ },
+ };
+ },
+ setSvg(name) {
+ return getSvgIconPathContent(name)
+ .then(path => {
+ if (path) {
+ this.$set(this.svgs, name, `path://${path}`);
+ }
+ })
+ .catch(() => {});
+ },
+ onMasterChartCreated(chart) {
+ this.masterChart = chart;
+ this.setSvg('scroll-handle')
+ .then(() => {
+ this.masterChart.setOption({
+ dataZoom: [
+ {
+ type: 'slider',
+ handleIcon: this.svgs['scroll-handle'],
+ },
+ ],
+ });
+ })
+ .catch(() => {});
+ this.masterChart.on('datazoom', _.debounce(this.setIndividualChartsZoom, 200));
+ },
+ onIndividualChartCreated(chart) {
+ this.individualCharts.push(chart);
+ },
+ setIndividualChartsZoom(options) {
+ this.charts.forEach(chart =>
+ chart.setOption(
+ {
+ dataZoom: {
+ start: options.start,
+ end: options.end,
+ show: false,
+ },
+ },
+ { lazyUpdate: true },
+ ),
+ );
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div v-if="loading" class="contributors-loader text-center">
+ <gl-loading-icon :inline="true" :size="4" />
+ </div>
+
+ <div v-else-if="showChart" class="contributors-charts">
+ <h4>{{ __('Commits to') }} {{ branch }}</h4>
+ <span>{{ __('Excluding merge commits. Limited to 6,000 commits.') }}</span>
+ <div>
+ <gl-area-chart
+ :data="masterChartData"
+ :option="masterChartOptions"
+ :height="masterChartHeight"
+ @created="onMasterChartCreated"
+ />
+ </div>
+
+ <div class="row">
+ <div v-for="contributor in individualChartsData" :key="contributor.name" class="col-6">
+ <h4>{{ contributor.name }}</h4>
+ <p>{{ n__('%d commit', '%d commits', contributor.commits) }} ({{ contributor.email }})</p>
+ <gl-area-chart
+ :data="contributor.dates"
+ :option="individualChartOptions"
+ :height="individualChartHeight"
+ @created="onIndividualChartCreated"
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/contributors/index.js b/app/assets/javascripts/contributors/index.js
new file mode 100644
index 00000000000..b6063589734
--- /dev/null
+++ b/app/assets/javascripts/contributors/index.js
@@ -0,0 +1,23 @@
+import Vue from 'vue';
+import ContributorsGraphs from './components/contributors.vue';
+import store from './stores';
+
+export default () => {
+ const el = document.querySelector('.js-contributors-graph');
+
+ if (!el) return null;
+
+ return new Vue({
+ el,
+ store,
+
+ render(createElement) {
+ return createElement(ContributorsGraphs, {
+ props: {
+ endpoint: el.dataset.projectGraphPath,
+ branch: el.dataset.projectBranch,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/contributors/services/contributors_service.js b/app/assets/javascripts/contributors/services/contributors_service.js
new file mode 100644
index 00000000000..5a8bbb66511
--- /dev/null
+++ b/app/assets/javascripts/contributors/services/contributors_service.js
@@ -0,0 +1,7 @@
+import axios from '~/lib/utils/axios_utils';
+
+export default {
+ fetchChartData(endpoint) {
+ return axios.get(endpoint);
+ },
+};
diff --git a/app/assets/javascripts/contributors/stores/actions.js b/app/assets/javascripts/contributors/stores/actions.js
new file mode 100644
index 00000000000..4138ff24f1d
--- /dev/null
+++ b/app/assets/javascripts/contributors/stores/actions.js
@@ -0,0 +1,20 @@
+import flash from '~/flash';
+import { __ } from '~/locale';
+import service from '../services/contributors_service';
+import * as types from './mutation_types';
+
+export const fetchChartData = ({ commit }, endpoint) => {
+ commit(types.SET_LOADING_STATE, true);
+
+ return service
+ .fetchChartData(endpoint)
+ .then(res => res.data)
+ .then(data => {
+ commit(types.SET_CHART_DATA, data);
+ commit(types.SET_LOADING_STATE, false);
+ })
+ .catch(() => flash(__('An error occurred while loading chart data')));
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/contributors/stores/getters.js b/app/assets/javascripts/contributors/stores/getters.js
new file mode 100644
index 00000000000..9e02e3ed9e7
--- /dev/null
+++ b/app/assets/javascripts/contributors/stores/getters.js
@@ -0,0 +1,33 @@
+export const showChart = state => Boolean(!state.loading && state.chartData);
+
+export const parsedData = state => {
+ const byAuthor = {};
+ const total = {};
+
+ state.chartData.forEach(({ date, author_name, author_email }) => {
+ total[date] = total[date] ? total[date] + 1 : 1;
+
+ const authorData = byAuthor[author_name];
+
+ if (!authorData) {
+ byAuthor[author_name] = {
+ email: author_email.toLowerCase(),
+ commits: 1,
+ dates: {
+ [date]: 1,
+ },
+ };
+ } else {
+ authorData.commits += 1;
+ authorData.dates[date] = authorData.dates[date] ? authorData.dates[date] + 1 : 1;
+ }
+ });
+
+ return {
+ total,
+ byAuthor,
+ };
+};
+
+// prevent babel-plugin-rewire from generating an invalid default during karma tests
+export default () => {};
diff --git a/app/assets/javascripts/contributors/stores/index.js b/app/assets/javascripts/contributors/stores/index.js
new file mode 100644
index 00000000000..bc739851aa7
--- /dev/null
+++ b/app/assets/javascripts/contributors/stores/index.js
@@ -0,0 +1,18 @@
+import Vue from 'vue';
+import Vuex from 'vuex';
+import state from './state';
+import mutations from './mutations';
+import * as getters from './getters';
+import * as actions from './actions';
+
+Vue.use(Vuex);
+
+export const createStore = () =>
+ new Vuex.Store({
+ actions,
+ mutations,
+ getters,
+ state: state(),
+ });
+
+export default createStore();
diff --git a/app/assets/javascripts/contributors/stores/mutation_types.js b/app/assets/javascripts/contributors/stores/mutation_types.js
new file mode 100644
index 00000000000..62e0a51d5f8
--- /dev/null
+++ b/app/assets/javascripts/contributors/stores/mutation_types.js
@@ -0,0 +1,3 @@
+export const SET_CHART_DATA = 'SET_CHART_DATA';
+export const SET_LOADING_STATE = 'SET_LOADING_STATE';
+export const SET_ACTIVE_BRANCH = 'SET_ACTIVE_BRANCH';
diff --git a/app/assets/javascripts/contributors/stores/mutations.js b/app/assets/javascripts/contributors/stores/mutations.js
new file mode 100644
index 00000000000..f1f460d072d
--- /dev/null
+++ b/app/assets/javascripts/contributors/stores/mutations.js
@@ -0,0 +1,17 @@
+import * as types from './mutation_types';
+
+export default {
+ [types.SET_LOADING_STATE](state, value) {
+ state.loading = value;
+ },
+ [types.SET_CHART_DATA](state, chartData) {
+ Object.assign(state, {
+ chartData,
+ });
+ },
+ [types.SET_ACTIVE_BRANCH](state, branch) {
+ Object.assign(state, {
+ branch,
+ });
+ },
+};
diff --git a/app/assets/javascripts/contributors/stores/state.js b/app/assets/javascripts/contributors/stores/state.js
new file mode 100644
index 00000000000..1dc1a3c7b75
--- /dev/null
+++ b/app/assets/javascripts/contributors/stores/state.js
@@ -0,0 +1,5 @@
+export default () => ({
+ loading: false,
+ chartData: null,
+ branch: 'master',
+});
diff --git a/app/assets/javascripts/contributors/utils.js b/app/assets/javascripts/contributors/utils.js
new file mode 100644
index 00000000000..7d8932ce495
--- /dev/null
+++ b/app/assets/javascripts/contributors/utils.js
@@ -0,0 +1,30 @@
+import { getMonthNames } from '~/lib/utils/datetime_utility';
+
+/**
+ * Converts provided string to date and returns formatted value as a year for date in January and month name for the rest
+ * @param {String}
+ * @returns {String} - formatted value
+ *
+ * xAxisLabelFormatter('01-12-2019') will return '2019'
+ * xAxisLabelFormatter('02-12-2019') will return 'Feb'
+ * xAxisLabelFormatter('07-12-2019') will return 'Jul'
+ */
+export const xAxisLabelFormatter = val => {
+ const date = new Date(val);
+ const month = date.getUTCMonth();
+ const year = date.getUTCFullYear();
+ return month === 0 ? `${year}` : getMonthNames(true)[month];
+};
+
+/**
+ * Formats provided date to YYYY-MM-DD format
+ * @param {Date}
+ * @returns {String} - formatted value
+ */
+export const dateFormatter = date => {
+ const year = date.getUTCFullYear();
+ const month = date.getUTCMonth();
+ const day = date.getUTCDate();
+
+ return `${year}-${`0${month + 1}`.slice(-2)}-${`0${day}`.slice(-2)}`;
+};