2019-12-26 22:10:19 +05:30
|
|
|
<script>
|
2023-04-23 21:23:45 +05:30
|
|
|
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
|
2019-12-26 22:10:19 +05:30
|
|
|
import { GlAreaChart } from '@gitlab/ui/dist/charts';
|
2021-03-11 19:13:27 +05:30
|
|
|
import { debounce, uniq } from 'lodash';
|
|
|
|
import { mapActions, mapState, mapGetters } from 'vuex';
|
2023-04-23 21:23:45 +05:30
|
|
|
import { visitUrl } from '~/lib/utils/url_utility';
|
2019-12-26 22:10:19 +05:30
|
|
|
import { getDatesInRange } from '~/lib/utils/datetime_utility';
|
2021-03-11 19:13:27 +05:30
|
|
|
import { getSvgIconPathContent } from '~/lib/utils/icon_utils';
|
|
|
|
import { __ } from '~/locale';
|
2023-04-23 21:23:45 +05:30
|
|
|
import RefSelector from '~/ref/components/ref_selector.vue';
|
|
|
|
import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants';
|
2021-03-11 19:13:27 +05:30
|
|
|
import { xAxisLabelFormatter, dateFormatter } from '../utils';
|
2019-12-26 22:10:19 +05:30
|
|
|
|
2023-04-23 21:23:45 +05:30
|
|
|
const GRAPHS_PATH_REGEX = /^(.*?)\/-\/graphs/g;
|
|
|
|
|
2019-12-26 22:10:19 +05:30
|
|
|
export default {
|
2023-04-23 21:23:45 +05:30
|
|
|
i18n: {
|
|
|
|
history: __('History'),
|
|
|
|
refSelectorTranslations: {
|
|
|
|
dropdownHeader: __('Switch branch/tag'),
|
|
|
|
searchPlaceholder: __('Search branches and tags'),
|
|
|
|
},
|
|
|
|
},
|
2019-12-26 22:10:19 +05:30
|
|
|
components: {
|
|
|
|
GlAreaChart,
|
2023-04-23 21:23:45 +05:30
|
|
|
GlButton,
|
2019-12-26 22:10:19 +05:30
|
|
|
GlLoadingIcon,
|
2023-04-23 21:23:45 +05:30
|
|
|
RefSelector,
|
2019-12-26 22:10:19 +05:30
|
|
|
},
|
|
|
|
props: {
|
|
|
|
endpoint: {
|
|
|
|
type: String,
|
|
|
|
required: true,
|
|
|
|
},
|
|
|
|
branch: {
|
|
|
|
type: String,
|
|
|
|
required: true,
|
|
|
|
},
|
2023-04-23 21:23:45 +05:30
|
|
|
projectId: {
|
|
|
|
type: String,
|
|
|
|
required: true,
|
|
|
|
},
|
|
|
|
commitsPath: {
|
|
|
|
type: String,
|
|
|
|
required: true,
|
|
|
|
},
|
2019-12-26 22:10:19 +05:30
|
|
|
},
|
2023-04-23 21:23:45 +05:30
|
|
|
refTypes: [REF_TYPE_BRANCHES, REF_TYPE_TAGS],
|
2019-12-26 22:10:19 +05:30
|
|
|
data() {
|
|
|
|
return {
|
|
|
|
masterChart: null,
|
|
|
|
individualCharts: [],
|
|
|
|
svgs: {},
|
|
|
|
masterChartHeight: 264,
|
|
|
|
individualChartHeight: 216,
|
2023-04-23 21:23:45 +05:30
|
|
|
selectedBranch: this.branch,
|
2019-12-26 22:10:19 +05:30
|
|
|
};
|
|
|
|
},
|
|
|
|
computed: {
|
|
|
|
...mapState(['chartData', 'loading']),
|
|
|
|
...mapGetters(['showChart', 'parsedData']),
|
|
|
|
masterChartData() {
|
|
|
|
const data = {};
|
2021-03-08 18:12:59 +05:30
|
|
|
this.xAxisRange.forEach((date) => {
|
2019-12-26 22:10:19 +05:30
|
|
|
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;
|
|
|
|
|
2020-04-08 14:13:33 +05:30
|
|
|
return Object.keys(this.parsedData.byAuthorEmail)
|
2021-03-08 18:12:59 +05:30
|
|
|
.map((email) => {
|
2020-04-08 14:13:33 +05:30
|
|
|
const author = this.parsedData.byAuthorEmail[email];
|
2019-12-26 22:10:19 +05:30
|
|
|
return {
|
2020-04-08 14:13:33 +05:30
|
|
|
name: author.name,
|
|
|
|
email,
|
2019-12-26 22:10:19 +05:30
|
|
|
commits: author.commits,
|
|
|
|
dates: [
|
|
|
|
{
|
|
|
|
name: __('Commits'),
|
2021-03-08 18:12:59 +05:30
|
|
|
data: this.xAxisRange.map((date) => [date, author.dates[date] || 0]),
|
2019-12-26 22:10:19 +05:30
|
|
|
},
|
|
|
|
],
|
|
|
|
};
|
|
|
|
})
|
|
|
|
.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) => {
|
2021-03-08 18:12:59 +05:30
|
|
|
const values = item.dates[0].data.map((value) => value[1]);
|
2019-12-26 22:10:19 +05:30
|
|
|
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() {
|
2020-03-13 15:44:24 +05:30
|
|
|
return uniq(this.individualCharts);
|
2019-12-26 22:10:19 +05:30
|
|
|
},
|
|
|
|
},
|
|
|
|
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)
|
2021-03-08 18:12:59 +05:30
|
|
|
.then((path) => {
|
2019-12-26 22:10:19 +05:30
|
|
|
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(() => {});
|
2020-03-13 15:44:24 +05:30
|
|
|
this.masterChart.on('datazoom', debounce(this.setIndividualChartsZoom, 200));
|
2019-12-26 22:10:19 +05:30
|
|
|
},
|
|
|
|
onIndividualChartCreated(chart) {
|
|
|
|
this.individualCharts.push(chart);
|
|
|
|
},
|
|
|
|
setIndividualChartsZoom(options) {
|
2021-03-08 18:12:59 +05:30
|
|
|
this.charts.forEach((chart) =>
|
2019-12-26 22:10:19 +05:30
|
|
|
chart.setOption(
|
|
|
|
{
|
|
|
|
dataZoom: {
|
|
|
|
start: options.start,
|
|
|
|
end: options.end,
|
|
|
|
show: false,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
{ lazyUpdate: true },
|
|
|
|
),
|
|
|
|
);
|
|
|
|
},
|
2023-04-23 21:23:45 +05:30
|
|
|
visitBranch(selected) {
|
|
|
|
const graphsPathPrefix = this.endpoint.match(GRAPHS_PATH_REGEX)?.[0];
|
|
|
|
|
|
|
|
visitUrl(`${graphsPathPrefix}/${selected}`);
|
|
|
|
},
|
2019-12-26 22:10:19 +05:30
|
|
|
},
|
|
|
|
};
|
|
|
|
</script>
|
|
|
|
|
|
|
|
<template>
|
|
|
|
<div>
|
2022-11-25 23:54:43 +05:30
|
|
|
<div v-if="loading" class="gl-text-center gl-pt-13">
|
2023-04-23 21:23:45 +05:30
|
|
|
<gl-loading-icon :inline="true" size="xl" data-testid="loading-app-icon" />
|
2019-12-26 22:10:19 +05:30
|
|
|
</div>
|
|
|
|
|
2023-04-23 21:23:45 +05:30
|
|
|
<template v-else-if="showChart">
|
|
|
|
<div class="gl-border-b gl-border-gray-100 gl-mb-6 gl-bg-gray-10 gl-p-5">
|
|
|
|
<div class="gl-display-flex">
|
|
|
|
<div class="gl-mr-3">
|
|
|
|
<ref-selector
|
|
|
|
v-model="selectedBranch"
|
|
|
|
:project-id="projectId"
|
|
|
|
:enabled-ref-types="$options.refTypes"
|
|
|
|
:translations="$options.i18n.refSelectorTranslations"
|
|
|
|
@input="visitBranch"
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
<gl-button :href="commitsPath" data-testid="history-button"
|
|
|
|
>{{ $options.i18n.history }}
|
|
|
|
</gl-button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div data-testid="contributors-charts">
|
|
|
|
<h4 class="gl-mb-2 gl-mt-5">{{ __('Commits to') }} {{ branch }}</h4>
|
|
|
|
<span>{{ __('Excluding merge commits. Limited to 6,000 commits.') }}</span>
|
2023-05-27 22:25:52 +05:30
|
|
|
<gl-area-chart
|
|
|
|
class="gl-mb-5"
|
|
|
|
responsive
|
|
|
|
width="auto"
|
|
|
|
:data="masterChartData"
|
|
|
|
:option="masterChartOptions"
|
|
|
|
:height="masterChartHeight"
|
|
|
|
@created="onMasterChartCreated"
|
|
|
|
/>
|
2019-12-26 22:10:19 +05:30
|
|
|
|
2023-04-23 21:23:45 +05:30
|
|
|
<div class="row">
|
|
|
|
<div
|
|
|
|
v-for="(contributor, index) in individualChartsData"
|
|
|
|
:key="index"
|
|
|
|
class="col-lg-6 col-12 gl-my-5"
|
|
|
|
>
|
|
|
|
<h4 class="gl-mb-2 gl-mt-0">{{ contributor.name }}</h4>
|
|
|
|
<p class="gl-mb-3">
|
|
|
|
{{ n__('%d commit', '%d commits', contributor.commits) }} ({{ contributor.email }})
|
|
|
|
</p>
|
2023-05-27 22:25:52 +05:30
|
|
|
<gl-area-chart
|
|
|
|
responsive
|
|
|
|
width="auto"
|
|
|
|
:data="contributor.dates"
|
|
|
|
:option="individualChartOptions"
|
|
|
|
:height="individualChartHeight"
|
|
|
|
@created="onIndividualChartCreated"
|
|
|
|
/>
|
2023-04-23 21:23:45 +05:30
|
|
|
</div>
|
2019-12-26 22:10:19 +05:30
|
|
|
</div>
|
|
|
|
</div>
|
2023-04-23 21:23:45 +05:30
|
|
|
</template>
|
2019-12-26 22:10:19 +05:30
|
|
|
</div>
|
|
|
|
</template>
|