mirror of https://github.com/go-gitea/gitea.git
Browse Source
This is the implementation of Recent Commits page. This feature was mentioned on #18262. It adds another tab to Activity page called Recent Commits. Recent Commits tab shows number of commits since last year for the repository.pull/29378/head
9 changed files with 233 additions and 1 deletions
@ -0,0 +1,41 @@
@@ -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 ( |
||||
tplRecentCommits base.TplName = "repo/activity" |
||||
) |
||||
|
||||
// RecentCommits renders the page to show recent commit frequency on repository
|
||||
func RecentCommits(ctx *context.Context) { |
||||
ctx.Data["Title"] = ctx.Tr("repo.activity.navbar.recent_commits") |
||||
|
||||
ctx.Data["PageIsActivity"] = true |
||||
ctx.Data["PageIsRecentCommits"] = true |
||||
ctx.PageData["repoLink"] = ctx.Repo.RepoLink |
||||
|
||||
ctx.HTML(http.StatusOK, tplRecentCommits) |
||||
} |
||||
|
||||
// RecentCommitsData returns JSON of recent commits data
|
||||
func RecentCommitsData(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("RecentCommitsData", err) |
||||
} else { |
||||
ctx.JSON(http.StatusOK, contributorStats["total"].Weeks) |
||||
} |
||||
} |
||||
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
{{if .Permission.CanRead $.UnitTypeCode}} |
||||
<div id="repo-recent-commits-chart" |
||||
data-locale-loading-title="{{ctx.Locale.Tr "graphs.component_loading" (ctx.Locale.Tr "graphs.recent_commits.what")}}" |
||||
data-locale-loading-title-failed="{{ctx.Locale.Tr "graphs.component_loading_failed" (ctx.Locale.Tr "graphs.recent_commits.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,149 @@
@@ -0,0 +1,149 @@
|
||||
<script> |
||||
import {SvgIcon} from '../svg.js'; |
||||
import { |
||||
Chart, |
||||
Tooltip, |
||||
BarElement, |
||||
LinearScale, |
||||
TimeScale, |
||||
} from 'chart.js'; |
||||
import {GET} from '../modules/fetch.js'; |
||||
import {Bar} 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, |
||||
BarElement, |
||||
Tooltip, |
||||
); |
||||
|
||||
export default { |
||||
components: {Bar, 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/recent-commits/data`); |
||||
if (response.status === 202) { |
||||
await sleep(1000); // wait for 1 second before retrying |
||||
} |
||||
} while (response.status === 202); |
||||
if (response.ok) { |
||||
const data = await response.json(); |
||||
const start = Object.values(data)[0].week; |
||||
const end = firstStartDateAfterDate(new Date()); |
||||
const startDays = startDaysBetween(new Date(start), new Date(end)); |
||||
this.data = fillEmptyStartDaysWithZeroes(startDays, data).slice(-52); |
||||
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.commits})), |
||||
label: 'Commits', |
||||
backgroundColor: chartJsColors['commits'], |
||||
borderWidth: 0, |
||||
tension: 0.3, |
||||
}, |
||||
], |
||||
}; |
||||
}, |
||||
|
||||
getOptions() { |
||||
return { |
||||
responsive: true, |
||||
maintainAspectRatio: false, |
||||
animation: true, |
||||
scales: { |
||||
x: { |
||||
type: 'time', |
||||
grid: { |
||||
display: false, |
||||
}, |
||||
time: { |
||||
minUnit: 'week', |
||||
}, |
||||
ticks: { |
||||
maxRotation: 0, |
||||
maxTicksLimit: 52 |
||||
}, |
||||
}, |
||||
y: { |
||||
ticks: { |
||||
maxTicksLimit: 6 |
||||
}, |
||||
}, |
||||
}, |
||||
}; |
||||
}, |
||||
}, |
||||
}; |
||||
</script> |
||||
<template> |
||||
<div> |
||||
<div class="ui header gt-df gt-ac gt-sb"> |
||||
{{ isLoading ? locale.loadingTitle : errorText ? locale.loadingTitleFailed: "Number of commits in the past year" }} |
||||
</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> |
||||
<Bar |
||||
v-memo="data" v-if="data.length !== 0" |
||||
:data="toGraphData(data)" :options="getOptions()" |
||||
/> |
||||
</div> |
||||
</div> |
||||
</template> |
||||
<style scoped> |
||||
.main-graph { |
||||
height: 250px; |
||||
} |
||||
</style> |
||||
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
import {createApp} from 'vue'; |
||||
|
||||
export async function initRepoRecentCommits() { |
||||
const el = document.getElementById('repo-recent-commits-chart'); |
||||
if (!el) return; |
||||
|
||||
const {default: RepoRecentCommits} = await import(/* webpackChunkName: "recent-commits-graph" */'../components/RepoRecentCommits.vue'); |
||||
try { |
||||
const View = createApp(RepoRecentCommits, { |
||||
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('RepoRecentCommits failed to load', err); |
||||
el.textContent = el.getAttribute('data-locale-component-failed-to-load'); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue