Commit b2fe5e99 authored by Mike Greiling's avatar Mike Greiling

Merge branch '63988-convert-contributors-graph-to-echarts' into 'master'

Contributors graphs migration to echarts

See merge request gitlab-org/gitlab!16677
parents 2f1f43ae 4adeaf23
<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>
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,
},
});
},
});
};
import axios from '~/lib/utils/axios_utils';
export default {
fetchChartData(endpoint) {
return axios.get(endpoint);
},
};
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 () => {};
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 () => {};
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();
export const SET_CHART_DATA = 'SET_CHART_DATA';
export const SET_LOADING_STATE = 'SET_LOADING_STATE';
export const SET_ACTIVE_BRANCH = 'SET_ACTIVE_BRANCH';
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,
});
},
};
export default () => ({
loading: false,
chartData: null,
branch: 'master',
});
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)}`;
};
...@@ -564,3 +564,26 @@ export const getDateInPast = (date, daysInPast) => { ...@@ -564,3 +564,26 @@ export const getDateInPast = (date, daysInPast) => {
export const beginOfDayTime = 'T00:00:00Z'; export const beginOfDayTime = 'T00:00:00Z';
export const endOfDayTime = 'T23:59:59Z'; export const endOfDayTime = 'T23:59:59Z';
/**
* @param {Date} d1
* @param {Date} d2
* @param {Function} formatter
* @return {Any[]} an array of formatted dates between 2 given dates (including start&end date)
*/
export const getDatesInRange = (d1, d2, formatter = x => x) => {
if (!(d1 instanceof Date) || !(d2 instanceof Date)) {
return [];
}
let startDate = d1.getTime();
const endDate = d2.getTime();
const oneDay = 24 * 3600 * 1000;
const range = [d1];
while (startDate < endDate) {
startDate += oneDay;
range.push(new Date(startDate));
}
return range.map(formatter);
};
import $ from 'jquery'; import initContributorsGraphs from '~/contributors';
import flash from '~/flash';
import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import ContributorsStatGraph from './stat_graph_contributors';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', initContributorsGraphs);
const url = document.querySelector('.js-graphs-show').dataset.projectGraphPath;
axios
.get(url)
.then(({ data }) => {
const graph = new ContributorsStatGraph();
graph.init(data);
$('#brush_change').change(() => {
graph.change_date_header();
graph.redraw_authors();
});
$('.stat-graph').fadeIn();
$('.loading-graph').hide();
})
.catch(() => flash(__('Error fetching contributors data.')));
});
/* eslint-disable func-names, no-var, one-var, camelcase, no-param-reassign, no-return-assign */
import $ from 'jquery';
import _ from 'underscore';
import { n__, s__, createDateTimeFormat, sprintf } from '~/locale';
import {
ContributorsGraph,
ContributorsAuthorGraph,
ContributorsMasterGraph,
} from './stat_graph_contributors_graph';
import ContributorsStatGraphUtil from './stat_graph_contributors_util';
export default (function() {
function ContributorsStatGraph() {
this.dateFormat = createDateTimeFormat({ year: 'numeric', month: 'long', day: 'numeric' });
}
ContributorsStatGraph.prototype.init = function(log) {
var author_commits, total_commits;
this.parsed_log = ContributorsStatGraphUtil.parse_log(log);
this.set_current_field('commits');
total_commits = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field);
author_commits = ContributorsStatGraphUtil.get_author_data(this.parsed_log, this.field);
this.add_master_graph(total_commits);
this.add_authors_graph(author_commits);
return this.change_date_header();
};
ContributorsStatGraph.prototype.add_master_graph = function(total_data) {
this.master_graph = new ContributorsMasterGraph(total_data);
return this.master_graph.draw();
};
ContributorsStatGraph.prototype.add_authors_graph = function(author_data) {
var limited_author_data;
this.authors = [];
limited_author_data = author_data.slice(0, 100);
return _.each(
limited_author_data,
(function(_this) {
return function(d) {
var author_graph, author_header;
author_header = _this.create_author_header(d);
$('.contributors-list').append(author_header);
author_graph = new ContributorsAuthorGraph(d.dates);
_this.authors[d.author_name] = author_graph;
return author_graph.draw();
};
})(this),
);
};
ContributorsStatGraph.prototype.format_author_commit_info = function(author) {
var commits;
commits = $('<span/>', {
class: 'graph-author-commits-count',
});
commits.text(n__('%d commit', '%d commits', author.commits));
return $('<span/>').append(commits);
};
ContributorsStatGraph.prototype.create_author_header = function(author) {
var author_commit_info, author_commit_info_span, author_email, author_name, list_item;
list_item = $('<li/>', {
class: 'person',
style: 'display: block;',
});
author_name = $(`<h4>${author.author_name}</h4>`);
author_email = $(`<p class="graph-author-email">${author.author_email}</p>`);
author_commit_info_span = $('<span/>', {
class: 'commits',
});
author_commit_info = this.format_author_commit_info(author);
author_commit_info_span.html(author_commit_info);
list_item.append(author_name);
list_item.append(author_email);
list_item.append(author_commit_info_span);
return list_item;
};
ContributorsStatGraph.prototype.redraw_master = function() {
var total_data;
total_data = ContributorsStatGraphUtil.get_total_data(this.parsed_log, this.field);
this.master_graph.set_data(total_data);
return this.master_graph.redraw();
};
ContributorsStatGraph.prototype.redraw_authors = function() {
$('ol').html('');
const { x_domain } = ContributorsGraph.prototype;
const author_commits = ContributorsStatGraphUtil.get_author_data(
this.parsed_log,
this.field,
x_domain,
);
return _.each(
author_commits,
(function(_this) {
return function(d) {
_this.redraw_author_commit_info(d);
if (_this.authors[d.author_name] != null) {
$(_this.authors[d.author_name].list_item).appendTo('ol');
_this.authors[d.author_name].set_data(d.dates);
return _this.authors[d.author_name].redraw();
}
return '';
};
})(this),
);
};
ContributorsStatGraph.prototype.set_current_field = function(field) {
return (this.field = field);
};
ContributorsStatGraph.prototype.change_date_header = function() {
const { x_domain } = ContributorsGraph.prototype;
const formattedDateRange = sprintf(s__('ContributorsPage|%{startDate} – %{endDate}'), {
startDate: this.dateFormat.format(new Date(x_domain[0])),
endDate: this.dateFormat.format(new Date(x_domain[1])),
});
return $('#date_header').text(formattedDateRange);
};
ContributorsStatGraph.prototype.redraw_author_commit_info = function(author) {
var author_commit_info, author_list_item, $author;
$author = this.authors[author.author_name];
if ($author != null) {
author_list_item = $(this.authors[author.author_name].list_item);
author_commit_info = this.format_author_commit_info(author);
return author_list_item.find('span').html(author_commit_info);
}
return '';
};
return ContributorsStatGraph;
})();
/* eslint-disable func-names, no-var, one-var, camelcase, no-param-reassign, no-return-assign, consistent-return, no-cond-assign, no-else-return */
import _ from 'underscore';
export default {
parse_log(log) {
var by_author, by_email, data, entry, i, len, total, normalized_email;
total = {};
by_author = {};
by_email = {};
for (i = 0, len = log.length; i < len; i += 1) {
entry = log[i];
if (total[entry.date] == null) {
this.add_date(entry.date, total);
}
normalized_email = entry.author_email.toLowerCase();
data = by_author[entry.author_name] || by_email[normalized_email];
if (data == null) {
data = this.add_author(entry, by_author, by_email);
}
if (!data[entry.date]) {
this.add_date(entry.date, data);
}
this.store_data(entry, total[entry.date], data[entry.date]);
}
total = _.toArray(total);
by_author = _.toArray(by_author);
return {
total,
by_author,
};
},
add_date(date, collection) {
collection[date] = {};
return (collection[date].date = date);
},
add_author(author, by_author, by_email) {
var data, normalized_email;
data = {};
data.author_name = author.author_name;
data.author_email = author.author_email;
normalized_email = author.author_email.toLowerCase();
by_author[author.author_name] = data;
by_email[normalized_email] = data;
return data;
},
store_data(entry, total, by_author) {
this.store_commits(total, by_author);
this.store_additions(entry, total, by_author);
return this.store_deletions(entry, total, by_author);
},
store_commits(total, by_author) {
this.add(total, 'commits', 1);
return this.add(by_author, 'commits', 1);
},
add(collection, field, value) {
if (collection[field] == null) {
collection[field] = 0;
}
return (collection[field] += value);
},
store_additions(entry, total, by_author) {
if (entry.additions == null) {
entry.additions = 0;
}
this.add(total, 'additions', entry.additions);
return this.add(by_author, 'additions', entry.additions);
},
store_deletions(entry, total, by_author) {
if (entry.deletions == null) {
entry.deletions = 0;
}
this.add(total, 'deletions', entry.deletions);
return this.add(by_author, 'deletions', entry.deletions);
},
get_total_data(parsed_log, field) {
var log, total_data;
log = parsed_log.total;
total_data = this.pick_field(log, field);
return _.sortBy(total_data, d => d.date);
},
pick_field(log, field) {
var total_data;
total_data = [];
_.each(log, d => total_data.push(_.pick(d, [field, 'date'])));
return total_data;
},
get_author_data(parsed_log, field, date_range) {
var author_data, log;
if (date_range == null) {
date_range = null;
}
log = parsed_log.by_author;
author_data = [];
_.each(
log,
(function(_this) {
return function(log_entry) {
var parsed_log_entry;
parsed_log_entry = _this.parse_log_entry(log_entry, field, date_range);
if (!_.isEmpty(parsed_log_entry.dates)) {
return author_data.push(parsed_log_entry);
}
};
})(this),
);
return _.sortBy(author_data, d => d[field]).reverse();
},
parse_log_entry(log_entry, field, date_range) {
var parsed_entry;
parsed_entry = {};
parsed_entry.author_name = log_entry.author_name;
parsed_entry.author_email = log_entry.author_email;
parsed_entry.dates = {};
parsed_entry.commits = 0;
parsed_entry.additions = 0;
parsed_entry.deletions = 0;
_.each(
_.omit(log_entry, 'author_name', 'author_email'),
(function(_this) {
return function(value) {
if (_this.in_range(value.date, date_range)) {
parsed_entry.dates[value.date] = value[field];
parsed_entry.commits += value.commits;
parsed_entry.additions += value.additions;
return (parsed_entry.deletions += value.deletions);
}
};
})(this),
);
return parsed_entry;
},
in_range(date, date_range) {
var ref;
if (date_range === null || (date_range[0] <= (ref = new Date(date)) && ref <= date_range[1])) {
return true;
} else {
return false;
}
},
};
...@@ -17,21 +17,6 @@ ...@@ -17,21 +17,6 @@
} }
} }
.graphs {
.graph-author-email {
float: right;
color: $gl-gray-500;
}
.graph-additions {
color: $green-600;
}
.graph-deletions {
color: $red-500;
}
}
.svg-graph-container { .svg-graph-container {
width: 100%; width: 100%;
......
.tint-box {
background: $stat-graph-common-bg;
position: relative;
margin-bottom: 10px;
}
.area {
fill: $green-500;
fill-opacity: 0.5;
}
.axis {
font-size: 10px;
}
#contributors-master {
@include media-breakpoint-up(md) {
@include make-col-ready();
@include make-col(12);
}
}
#contributors {
flex: 1;
.contributors-list {
margin: 0 0 10px;
list-style: none;
padding: 0;
}
.person {
@include media-breakpoint-up(md) {
@include make-col-ready();
@include make-col(6);
}
margin-top: 10px;
@include media-breakpoint-down(xs) {
width: 100%;
}
.spark {
display: block;
background: $stat-graph-common-bg;
width: 100%;
}
.area-contributor {
fill: $orange-500;
}
}
}
.selection rect {
fill-opacity: 0.1;
stroke-width: 1px;
stroke-opacity: 0.4;
shape-rendering: crispedges;
stroke-dasharray: 3 3;
}
- page_title _('Contributors') - page_title _('Contributors')
.js-graphs-show{ 'data-project-graph-path': project_graph_path(@project, current_ref, format: :json) } .sub-header-block.bg-gray-light.gl-p-3
.sub-header-block .tree-ref-holder.inline.vertical-align-middle
.tree-ref-holder.inline.vertical-align-middle = render 'shared/ref_switcher', destination: 'graphs'
= render 'shared/ref_switcher', destination: 'graphs' = link_to s_('Commits|History'), project_commits_path(@project, current_ref), class: 'btn'
= link_to s_('Commits|History'), project_commits_path(@project, current_ref), class: 'btn'
.loading-graph .js-contributors-graph{ class: container_class, 'data-project-graph-path': project_graph_path(@project, current_ref, format: :json),'data-project-branch': current_ref }
.center
%h3.page-title
%i.fa.fa-spinner.fa-spin
= s_('ContributorsPage|Building repository graph.')
%p.slead
= s_('ContributorsPage|Please wait a moment, this page will automatically refresh when ready.')
.stat-graph.hide
.header.clearfix
%h3#date_header.page-title
%p.light
= s_('ContributorsPage|Commits to %{branch_name}, excluding merge commits. Limited to 6,000 commits.') % { branch_name: @ref }
%input#brush_change{ :type => "hidden" }
.graphs.row
#contributors-master.svg-w-100
#contributors.clearfix
%ol.contributors-list.svg-w-100.row
.contributors {
&-loader {
padding-top: $header-height * 2;
}
}
---
title: Migrated contributors charts to echarts
merge_request: 16677
author:
type: other
...@@ -4226,6 +4226,9 @@ msgstr "" ...@@ -4226,6 +4226,9 @@ msgstr ""
msgid "Commits per weekday" msgid "Commits per weekday"
msgstr "" msgstr ""
msgid "Commits to"
msgstr ""
msgid "Commits|An error occurred while fetching merge requests data." msgid "Commits|An error occurred while fetching merge requests data."
msgstr "" msgstr ""
...@@ -4510,18 +4513,6 @@ msgstr "" ...@@ -4510,18 +4513,6 @@ msgstr ""
msgid "Contributors" msgid "Contributors"
msgstr "" msgstr ""
msgid "ContributorsPage|%{startDate} – %{endDate}"
msgstr ""
msgid "ContributorsPage|Building repository graph."
msgstr ""
msgid "ContributorsPage|Commits to %{branch_name}, excluding merge commits. Limited to 6,000 commits."
msgstr ""
msgid "ContributorsPage|Please wait a moment, this page will automatically refresh when ready."
msgstr ""
msgid "Control emails linked to your account" msgid "Control emails linked to your account"
msgstr "" msgstr ""
...@@ -6465,9 +6456,6 @@ msgstr "" ...@@ -6465,9 +6456,6 @@ msgstr ""
msgid "Error deleting %{issuableType}" msgid "Error deleting %{issuableType}"
msgstr "" msgstr ""
msgid "Error fetching contributors data."
msgstr ""
msgid "Error fetching diverging counts for branches. Please try again." msgid "Error fetching diverging counts for branches. Please try again."
msgstr "" msgstr ""
...@@ -6705,6 +6693,9 @@ msgstr "" ...@@ -6705,6 +6693,9 @@ msgstr ""
msgid "Except policy:" msgid "Except policy:"
msgstr "" msgstr ""
msgid "Excluding merge commits. Limited to 6,000 commits."
msgstr ""
msgid "Existing" msgid "Existing"
msgstr "" msgstr ""
...@@ -11322,6 +11313,9 @@ msgstr "" ...@@ -11322,6 +11313,9 @@ msgstr ""
msgid "Number of changes (branches or tags) in a single push to determine whether webhooks and services will be fired or not. Webhooks and services won't be submitted if it surpasses that value." msgid "Number of changes (branches or tags) in a single push to determine whether webhooks and services will be fired or not. Webhooks and services won't be submitted if it surpasses that value."
msgstr "" msgstr ""
msgid "Number of commits"
msgstr ""
msgid "Number of commits per MR" msgid "Number of commits per MR"
msgstr "" msgstr ""
......
...@@ -29,12 +29,6 @@ describe 'Project Graph', :js do ...@@ -29,12 +29,6 @@ describe 'Project Graph', :js do
end end
end end
it 'renders graphs' do
visit project_graph_path(project, 'master')
expect(page).to have_selector('.stat-graph', visible: false)
end
context 'commits graph' do context 'commits graph' do
before do before do
visit commits_project_graph_path(project, 'master') visit commits_project_graph_path(project, 'master')
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Contributors charts should render charts when loading completed and there is chart data 1`] = `
<div>
<div
class="contributors-charts"
>
<h4>
Commits to master
</h4>
<span>
Excluding merge commits. Limited to 6,000 commits.
</span>
<div>
<glareachart-stub
data="[object Object]"
height="264"
option="[object Object]"
/>
</div>
<div
class="row"
>
<div
class="col-6"
>
<h4>
John
</h4>
<p>
2 commits (jawnnypoo@gmail.com)
</p>
<glareachart-stub
data="[object Object]"
height="216"
option="[object Object]"
/>
</div>
</div>
</div>
</div>
`;
import Vue from 'vue';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { createStore } from '~/contributors/stores';
import axios from '~/lib/utils/axios_utils';
import MockAdapter from 'axios-mock-adapter';
import ContributorsCharts from '~/contributors/components/contributors.vue';
const localVue = createLocalVue();
let wrapper;
let mock;
let store;
const Component = Vue.extend(ContributorsCharts);
const endpoint = 'contributors';
const branch = 'master';
const chartData = [
{ author_name: 'John', author_email: 'jawnnypoo@gmail.com', date: '2019-05-05' },
{ author_name: 'John', author_email: 'jawnnypoo@gmail.com', date: '2019-03-03' },
];
function factory() {
mock = new MockAdapter(axios);
jest.spyOn(axios, 'get');
mock.onGet().reply(200, chartData);
store = createStore();
wrapper = shallowMount(Component, {
propsData: {
endpoint,
branch,
},
stubs: {
GlLoadingIcon: true,
GlAreaChart: true,
},
store,
});
}
describe('Contributors charts', () => {
beforeEach(() => {
factory();
});
afterEach(() => {
mock.restore();
wrapper.destroy();
});
it('should fetch chart data when mounted', () => {
expect(axios.get).toHaveBeenCalledWith(endpoint);
});
it('should display loader whiled loading data', () => {
wrapper.vm.$store.state.loading = true;
return localVue.nextTick(() => {
expect(wrapper.find('.contributors-loader').exists()).toBe(true);
});
});
it('should render charts when loading completed and there is chart data', () => {
wrapper.vm.$store.state.loading = false;
wrapper.vm.$store.state.chartData = chartData;
return localVue.nextTick(() => {
expect(wrapper.find('.contributors-loader').exists()).toBe(false);
expect(wrapper.find('.contributors-charts').exists()).toBe(true);
expect(wrapper.element).toMatchSnapshot();
});
});
});
import axios from '~/lib/utils/axios_utils';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import flashError from '~/flash';
import * as actions from '~/contributors/stores/actions';
import * as types from '~/contributors/stores/mutation_types';
jest.mock('~/flash.js');
describe('Contributors store actions', () => {
describe('fetchChartData', () => {
let mock;
const endpoint = '/contributors';
const chartData = { '2017-11': 0, '2017-12': 2 };
beforeEach(() => {
mock = new MockAdapter(axios);
});
it('should commit SET_CHART_DATA with received response', done => {
mock.onGet().reply(200, chartData);
testAction(
actions.fetchChartData,
{ endpoint },
{},
[
{ type: types.SET_LOADING_STATE, payload: true },
{ type: types.SET_CHART_DATA, payload: chartData },
{ type: types.SET_LOADING_STATE, payload: false },
],
[],
() => {
mock.restore();
done();
},
);
});
it('should show flash on API error', done => {
mock.onGet().reply(400, 'Not Found');
testAction(
actions.fetchChartData,
{ endpoint },
{},
[{ type: types.SET_LOADING_STATE, payload: true }],
[],
() => {
expect(flashError).toHaveBeenCalledWith(expect.stringMatching('error'));
mock.restore();
done();
},
);
});
});
});
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
import * as getters from '~/contributors/stores/getters';
describe('Contributors Store Getters', () => {
const state = {};
describe('showChart', () => {
it('should NOT show chart if loading', () => {
state.loading = true;
expect(getters.showChart(state)).toEqual(false);
});
it('should NOT show chart there is not data', () => {
state.loading = false;
state.chartData = null;
expect(getters.showChart(state)).toEqual(false);
});
it('should show the chart in case loading complated and there is data', () => {
state.loading = false;
state.chartData = true;
expect(getters.showChart(state)).toEqual(true);
});
describe('parsedData', () => {
let parsed;
beforeAll(() => {
state.chartData = [
{ author_name: 'John', author_email: 'jawnnypoo@gmail.com', date: '2019-05-05' },
{ author_name: 'John', author_email: 'jawnnypoo@gmail.com', date: '2019-05-05' },
{ author_name: 'Carlson', author_email: 'jawnnypoo@gmail.com', date: '2019-03-03' },
{ author_name: 'Carlson', author_email: 'jawnnypoo@gmail.com', date: '2019-05-05' },
{ author_name: 'John', author_email: 'jawnnypoo@gmail.com', date: '2019-04-04' },
{ author_name: 'John', author_email: 'jawnnypoo@gmail.com', date: '2019-04-04' },
{ author_name: 'John', author_email: 'jawnnypoo@gmail.com', date: '2019-03-03' },
];
parsed = getters.parsedData(state);
});
it('should group contributions by date ', () => {
expect(parsed.total).toMatchObject({ '2019-05-05': 3, '2019-03-03': 2, '2019-04-04': 2 });
});
it('should group contributions by author ', () => {
expect(parsed.byAuthor).toMatchObject({
Carlson: {
email: 'jawnnypoo@gmail.com',
commits: 2,
dates: {
'2019-03-03': 1,
'2019-05-05': 1,
},
},
John: {
email: 'jawnnypoo@gmail.com',
commits: 5,
dates: {
'2019-03-03': 1,
'2019-04-04': 2,
'2019-05-05': 2,
},
},
});
});
});
});
});
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
import state from '~/contributors/stores/state';
import mutations from '~/contributors/stores/mutations';
import * as types from '~/contributors/stores/mutation_types';
describe('Contributors mutations', () => {
let stateCopy;
beforeEach(() => {
stateCopy = state();
});
describe('SET_LOADING_STATE', () => {
it('should set loading flag', () => {
const loading = true;
mutations[types.SET_LOADING_STATE](stateCopy, loading);
expect(stateCopy.loading).toEqual(loading);
});
});
describe('SET_CHART_DATA', () => {
const chartData = { '2017-11': 0, '2017-12': 2 };
it('should set chart data', () => {
mutations[types.SET_CHART_DATA](stateCopy, chartData);
expect(stateCopy.chartData).toEqual(chartData);
});
});
describe('SET_ACTIVE_BRANCH', () => {
it('should set search query', () => {
const branch = 'feature-branch';
mutations[types.SET_ACTIVE_BRANCH](stateCopy, branch);
expect(stateCopy.branch).toEqual(branch);
});
});
});
import * as utils from '~/contributors/utils';
describe('Contributors Util Functions', () => {
describe('xAxisLabelFormatter', () => {
it('should return year if the date is in January', () => {
expect(utils.xAxisLabelFormatter(new Date('01-12-2019'))).toEqual('2019');
});
it('should return month name otherwise', () => {
expect(utils.xAxisLabelFormatter(new Date('12-02-2019'))).toEqual('Dec');
expect(utils.xAxisLabelFormatter(new Date('07-12-2019'))).toEqual('Jul');
});
});
describe('dateFormatter', () => {
it('should format provided date to YYYY-MM-DD format', () => {
expect(utils.dateFormatter(new Date('December 17, 1995 03:24:00'))).toEqual('1995-12-17');
expect(utils.dateFormatter(new Date(1565308800000))).toEqual('2019-08-09');
});
});
});
...@@ -441,3 +441,34 @@ describe('getDateInPast', () => { ...@@ -441,3 +441,34 @@ describe('getDateInPast', () => {
expect(date).toStrictEqual(new Date(1563235200000)); expect(date).toStrictEqual(new Date(1563235200000));
}); });
}); });
describe('getDatesInRange', () => {
it('returns an empty array if 1st or 2nd argument is not a Date object', () => {
const d1 = new Date('2019-01-01');
const d2 = 90;
const range = datetimeUtility.getDatesInRange(d1, d2);
expect(range).toEqual([]);
});
it('returns a range of dates between two given dates', () => {
const d1 = new Date('2019-01-01');
const d2 = new Date('2019-01-31');
const range = datetimeUtility.getDatesInRange(d1, d2);
expect(range.length).toEqual(31);
});
it('applies mapper function if provided fro each item in range', () => {
const d1 = new Date('2019-01-01');
const d2 = new Date('2019-01-31');
const formatter = date => date.getDate();
const range = datetimeUtility.getDatesInRange(d1, d2, formatter);
range.forEach((formattedItem, index) => {
expect(formattedItem).toEqual(index + 1);
});
});
});
/* eslint-disable jasmine/no-suite-dupes, vars-on-top, no-var */
import { scaleLinear, scaleTime } from 'd3-scale';
import { timeParse } from 'd3-time-format';
import {
ContributorsGraph,
ContributorsMasterGraph,
} from '~/pages/projects/graphs/show/stat_graph_contributors_graph';
const d3 = { scaleLinear, scaleTime, timeParse };
describe('ContributorsGraph', function() {
describe('#set_x_domain', function() {
it('set the x_domain', function() {
ContributorsGraph.set_x_domain(20);
expect(ContributorsGraph.prototype.x_domain).toEqual(20);
});
});
describe('#set_y_domain', function() {
it('sets the y_domain', function() {
ContributorsGraph.set_y_domain([{ commits: 30 }]);
expect(ContributorsGraph.prototype.y_domain).toEqual([0, 30]);
});
});
describe('#init_x_domain', function() {
it('sets the initial x_domain', function() {
ContributorsGraph.init_x_domain([{ date: '2013-01-31' }, { date: '2012-01-31' }]);
expect(ContributorsGraph.prototype.x_domain).toEqual(['2012-01-31', '2013-01-31']);
});
});
describe('#init_y_domain', function() {
it('sets the initial y_domain', function() {
ContributorsGraph.init_y_domain([{ commits: 30 }]);
expect(ContributorsGraph.prototype.y_domain).toEqual([0, 30]);
});
});
describe('#init_domain', function() {
it('calls init_x_domain and init_y_domain', function() {
spyOn(ContributorsGraph, 'init_x_domain');
spyOn(ContributorsGraph, 'init_y_domain');
ContributorsGraph.init_domain();
expect(ContributorsGraph.init_x_domain).toHaveBeenCalled();
expect(ContributorsGraph.init_y_domain).toHaveBeenCalled();
});
});
describe('#set_dates', function() {
it('sets the dates', function() {
ContributorsGraph.set_dates('2013-12-01');
expect(ContributorsGraph.prototype.dates).toEqual('2013-12-01');
});
});
describe('#set_x_domain', function() {
it("sets the instance's x domain using the prototype's x_domain", function() {
ContributorsGraph.prototype.x_domain = 20;
var instance = new ContributorsGraph();
instance.x = d3
.scaleTime()
.range([0, 100])
.clamp(true);
spyOn(instance.x, 'domain');
instance.set_x_domain();
expect(instance.x.domain).toHaveBeenCalledWith(20);
});
});
describe('#set_y_domain', function() {
it("sets the instance's y domain using the prototype's y_domain", function() {
ContributorsGraph.prototype.y_domain = 30;
var instance = new ContributorsGraph();
instance.y = d3
.scaleLinear()
.range([100, 0])
.nice();
spyOn(instance.y, 'domain');
instance.set_y_domain();
expect(instance.y.domain).toHaveBeenCalledWith(30);
});
});
describe('#set_domain', function() {
it('calls set_x_domain and set_y_domain', function() {
var instance = new ContributorsGraph();
spyOn(instance, 'set_x_domain');
spyOn(instance, 'set_y_domain');
instance.set_domain();
expect(instance.set_x_domain).toHaveBeenCalled();
expect(instance.set_y_domain).toHaveBeenCalled();
});
});
describe('#set_data', function() {
it('sets the data', function() {
var instance = new ContributorsGraph();
instance.set_data('20');
expect(instance.data).toEqual('20');
});
});
});
describe('ContributorsMasterGraph', function() {
// TODO: fix or remove
// describe("#process_dates", function () {
// it("gets and parses dates", function () {
// var graph = new ContributorsMasterGraph();
// var data = 'random data here';
// spyOn(graph, 'parse_dates');
// spyOn(graph, 'get_dates').andReturn("get");
// spyOn(ContributorsGraph,'set_dates').andCallThrough();
// graph.process_dates(data);
// expect(graph.parse_dates).toHaveBeenCalledWith(data);
// expect(graph.get_dates).toHaveBeenCalledWith(data);
// expect(ContributorsGraph.set_dates).toHaveBeenCalledWith("get");
// });
// });
describe('#get_dates', function() {
it('plucks the date field from data collection', function() {
var graph = new ContributorsMasterGraph();
var data = [{ date: '2013-01-01' }, { date: '2012-12-15' }];
expect(graph.get_dates(data)).toEqual(['2013-01-01', '2012-12-15']);
});
});
describe('#parse_dates', function() {
it('parses the dates', function() {
var graph = new ContributorsMasterGraph();
var parseDate = d3.timeParse('%Y-%m-%d');
var data = [{ date: '2013-01-01' }, { date: '2012-12-15' }];
var correct = [{ date: parseDate(data[0].date) }, { date: parseDate(data[1].date) }];
graph.parse_dates(data);
expect(data).toEqual(correct);
});
});
});
import ContributorsStatGraph from '~/pages/projects/graphs/show/stat_graph_contributors';
import { ContributorsGraph } from '~/pages/projects/graphs/show/stat_graph_contributors_graph';
import { setLanguage } from '../helpers/locale_helper';
describe('ContributorsStatGraph', () => {
describe('change_date_header', () => {
beforeAll(() => {
setLanguage('de');
});
afterAll(() => {
setLanguage(null);
});
it('uses the locale to display date ranges', () => {
ContributorsGraph.init_x_domain([{ date: '2013-01-31' }, { date: '2012-01-31' }]);
setFixtures('<div id="date_header"></div>');
const graph = new ContributorsStatGraph();
graph.change_date_header();
expect(document.getElementById('date_header').innerText).toBe(
'31. Januar 2012 – 31. Januar 2013',
);
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment