mirror of https://github.com/go-gitea/gitea.git
Implement code frequency graph (#29191)
### Overview This is the implementation of Code Frequency page. This feature was mentioned on these issues: #18262, #7392. It adds another tab to Activity page called Code Frequency. Code Frequency tab shows additions and deletions over time since the repository existed. Before: <img width="1296" alt="image" src="https://github.com/go-gitea/gitea/assets/32161460/2603504f-aee7-4929-a8c4-fb3412a7a0f6"> After: <img width="1296" alt="image" src="https://github.com/go-gitea/gitea/assets/32161460/58c03721-729f-4536-a663-9f337f240963"> --- #### Features - See additions deletions over time since repository existed - Click on "Additions" or "Deletions" legend to show only one type of contribution - Use the same cache from Contributors page so that the loading of data will be fast once it is cached by visiting either one of the pages --------- Co-authored-by: Giteabot <teabot@gitea.io>pull/29358/head
parent
6f6120dfa8
commit
875f5ea6d8
@ -0,0 +1,41 @@
|
||||
// Copyright 2023 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/context"
|
||||
contributors_service "code.gitea.io/gitea/services/repository"
|
||||
)
|
||||
|
||||
const (
|
||||
tplCodeFrequency base.TplName = "repo/activity"
|
||||
)
|
||||
|
||||
// CodeFrequency renders the page to show repository code frequency
|
||||
func CodeFrequency(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.activity.navbar.code_frequency")
|
||||
|
||||
ctx.Data["PageIsActivity"] = true
|
||||
ctx.Data["PageIsCodeFrequency"] = true
|
||||
ctx.PageData["repoLink"] = ctx.Repo.RepoLink
|
||||
|
||||
ctx.HTML(http.StatusOK, tplCodeFrequency)
|
||||
}
|
||||
|
||||
// CodeFrequencyData returns JSON of code frequency data
|
||||
func CodeFrequencyData(ctx *context.Context) {
|
||||
if contributorStats, err := contributors_service.GetContributorStats(ctx, ctx.Cache, ctx.Repo.Repository, ctx.Repo.CommitID); err != nil {
|
||||
if errors.Is(err, contributors_service.ErrAwaitGeneration) {
|
||||
ctx.Status(http.StatusAccepted)
|
||||
return
|
||||
}
|
||||
ctx.ServerError("GetCodeFrequencyData", err)
|
||||
} else {
|
||||
ctx.JSON(http.StatusOK, contributorStats["total"].Weeks)
|
||||
}
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
{{if .Permission.CanRead $.UnitTypeCode}}
|
||||
<div id="repo-code-frequency-chart"
|
||||
data-locale-loading-title="{{ctx.Locale.Tr "graphs.component_loading" (ctx.Locale.Tr "graphs.code_frequency.what")}}"
|
||||
data-locale-loading-title-failed="{{ctx.Locale.Tr "graphs.component_loading_failed" (ctx.Locale.Tr "graphs.code_frequency.what")}}"
|
||||
data-locale-loading-info="{{ctx.Locale.Tr "graphs.component_loading_info"}}"
|
||||
data-locale-component-failed-to-load="{{ctx.Locale.Tr "graphs.component_failed_to_load"}}"
|
||||
>
|
||||
</div>
|
||||
{{end}}
|
@ -0,0 +1,172 @@
|
||||
<script>
|
||||
import {SvgIcon} from '../svg.js';
|
||||
import {
|
||||
Chart,
|
||||
Legend,
|
||||
LinearScale,
|
||||
TimeScale,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Filler,
|
||||
} from 'chart.js';
|
||||
import {GET} from '../modules/fetch.js';
|
||||
import {Line as ChartLine} from 'vue-chartjs';
|
||||
import {
|
||||
startDaysBetween,
|
||||
firstStartDateAfterDate,
|
||||
fillEmptyStartDaysWithZeroes,
|
||||
} from '../utils/time.js';
|
||||
import {chartJsColors} from '../utils/color.js';
|
||||
import {sleep} from '../utils.js';
|
||||
import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
|
||||
|
||||
const {pageData} = window.config;
|
||||
|
||||
Chart.defaults.color = chartJsColors.text;
|
||||
Chart.defaults.borderColor = chartJsColors.border;
|
||||
|
||||
Chart.register(
|
||||
TimeScale,
|
||||
LinearScale,
|
||||
Legend,
|
||||
PointElement,
|
||||
LineElement,
|
||||
Filler,
|
||||
);
|
||||
|
||||
export default {
|
||||
components: {ChartLine, SvgIcon},
|
||||
props: {
|
||||
locale: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
},
|
||||
data: () => ({
|
||||
isLoading: false,
|
||||
errorText: '',
|
||||
repoLink: pageData.repoLink || [],
|
||||
data: [],
|
||||
}),
|
||||
mounted() {
|
||||
this.fetchGraphData();
|
||||
},
|
||||
methods: {
|
||||
async fetchGraphData() {
|
||||
this.isLoading = true;
|
||||
try {
|
||||
let response;
|
||||
do {
|
||||
response = await GET(`${this.repoLink}/activity/code-frequency/data`);
|
||||
if (response.status === 202) {
|
||||
await sleep(1000); // wait for 1 second before retrying
|
||||
}
|
||||
} while (response.status === 202);
|
||||
if (response.ok) {
|
||||
this.data = await response.json();
|
||||
const weekValues = Object.values(this.data);
|
||||
const start = weekValues[0].week;
|
||||
const end = firstStartDateAfterDate(new Date());
|
||||
const startDays = startDaysBetween(new Date(start), new Date(end));
|
||||
this.data = fillEmptyStartDaysWithZeroes(startDays, this.data);
|
||||
this.errorText = '';
|
||||
} else {
|
||||
this.errorText = response.statusText;
|
||||
}
|
||||
} catch (err) {
|
||||
this.errorText = err.message;
|
||||
} finally {
|
||||
this.isLoading = false;
|
||||
}
|
||||
},
|
||||
|
||||
toGraphData(data) {
|
||||
return {
|
||||
datasets: [
|
||||
{
|
||||
data: data.map((i) => ({x: i.week, y: i.additions})),
|
||||
pointRadius: 0,
|
||||
pointHitRadius: 0,
|
||||
fill: true,
|
||||
label: 'Additions',
|
||||
backgroundColor: chartJsColors['additions'],
|
||||
borderWidth: 0,
|
||||
tension: 0.3,
|
||||
},
|
||||
{
|
||||
data: data.map((i) => ({x: i.week, y: -i.deletions})),
|
||||
pointRadius: 0,
|
||||
pointHitRadius: 0,
|
||||
fill: true,
|
||||
label: 'Deletions',
|
||||
backgroundColor: chartJsColors['deletions'],
|
||||
borderWidth: 0,
|
||||
tension: 0.3,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
getOptions() {
|
||||
return {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
animation: true,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: true,
|
||||
},
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
type: 'time',
|
||||
grid: {
|
||||
display: false,
|
||||
},
|
||||
time: {
|
||||
minUnit: 'month',
|
||||
},
|
||||
ticks: {
|
||||
maxRotation: 0,
|
||||
maxTicksLimit: 12
|
||||
},
|
||||
},
|
||||
y: {
|
||||
ticks: {
|
||||
maxTicksLimit: 6
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<div class="ui header gt-df gt-ac gt-sb">
|
||||
{{ isLoading ? locale.loadingTitle : errorText ? locale.loadingTitleFailed: `Code frequency over the history of ${repoLink.slice(1)}` }}
|
||||
</div>
|
||||
<div class="gt-df ui segment main-graph">
|
||||
<div v-if="isLoading || errorText !== ''" class="gt-tc gt-m-auto">
|
||||
<div v-if="isLoading">
|
||||
<SvgIcon name="octicon-sync" class="gt-mr-3 job-status-rotate"/>
|
||||
{{ locale.loadingInfo }}
|
||||
</div>
|
||||
<div v-else class="text red">
|
||||
<SvgIcon name="octicon-x-circle-fill"/>
|
||||
{{ errorText }}
|
||||
</div>
|
||||
</div>
|
||||
<ChartLine
|
||||
v-memo="data" v-if="data.length !== 0"
|
||||
:data="toGraphData(data)" :options="getOptions()"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style scoped>
|
||||
.main-graph {
|
||||
height: 440px;
|
||||
}
|
||||
</style>
|
@ -0,0 +1,21 @@
|
||||
import {createApp} from 'vue';
|
||||
|
||||
export async function initRepoCodeFrequency() {
|
||||
const el = document.getElementById('repo-code-frequency-chart');
|
||||
if (!el) return;
|
||||
|
||||
const {default: RepoCodeFrequency} = await import(/* webpackChunkName: "code-frequency-graph" */'../components/RepoCodeFrequency.vue');
|
||||
try {
|
||||
const View = createApp(RepoCodeFrequency, {
|
||||
locale: {
|
||||
loadingTitle: el.getAttribute('data-locale-loading-title'),
|
||||
loadingTitleFailed: el.getAttribute('data-locale-loading-title-failed'),
|
||||
loadingInfo: el.getAttribute('data-locale-loading-info'),
|
||||
}
|
||||
});
|
||||
View.mount(el);
|
||||
} catch (err) {
|
||||
console.error('RepoCodeFrequency failed to load', err);
|
||||
el.textContent = el.getAttribute('data-locale-component-failed-to-load');
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue