summaryrefslogtreecommitdiffstats
path: root/web_src
diff options
context:
space:
mode:
authorŞahin Akkaya <sahin@sahinakkaya.dev>2024-02-24 02:41:24 +0300
committerGitHub <noreply@github.com>2024-02-23 23:41:24 +0000
commit875f5ea6d83c8371f309df99654ca3556623004c (patch)
tree24fc4b99a2b9c0ef60205f63dc0c5ccf3aa40eab /web_src
parent6f6120dfa8d549d0b866eeb9317054fea831c844 (diff)
downloadgitea-875f5ea6d83c8371f309df99654ca3556623004c.tar.gz
gitea-875f5ea6d83c8371f309df99654ca3556623004c.zip
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>
Diffstat (limited to 'web_src')
-rw-r--r--web_src/js/components/RepoCodeFrequency.vue172
-rw-r--r--web_src/js/components/RepoContributors.vue36
-rw-r--r--web_src/js/features/code-frequency.js21
-rw-r--r--web_src/js/index.js2
-rw-r--r--web_src/js/utils.js2
-rw-r--r--web_src/js/utils/color.js14
6 files changed, 217 insertions, 30 deletions
diff --git a/web_src/js/components/RepoCodeFrequency.vue b/web_src/js/components/RepoCodeFrequency.vue
new file mode 100644
index 0000000000..ad607a041a
--- /dev/null
+++ b/web_src/js/components/RepoCodeFrequency.vue
@@ -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>
diff --git a/web_src/js/components/RepoContributors.vue b/web_src/js/components/RepoContributors.vue
index fa1545b3df..84fdcae1f6 100644
--- a/web_src/js/components/RepoContributors.vue
+++ b/web_src/js/components/RepoContributors.vue
@@ -3,10 +3,7 @@ import {SvgIcon} from '../svg.js';
import {
Chart,
Title,
- Tooltip,
- Legend,
BarElement,
- CategoryScale,
LinearScale,
TimeScale,
PointElement,
@@ -21,27 +18,13 @@ import {
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';
import $ from 'jquery';
const {pageData} = window.config;
-const colors = {
- text: '--color-text',
- border: '--color-secondary-alpha-60',
- commits: '--color-primary-alpha-60',
- additions: '--color-green',
- deletions: '--color-red',
- title: '--color-secondary-dark-4',
-};
-
-const styles = window.getComputedStyle(document.documentElement);
-const getColor = (name) => styles.getPropertyValue(name).trim();
-
-for (const [key, value] of Object.entries(colors)) {
- colors[key] = getColor(value);
-}
-
const customEventListener = {
id: 'customEventListener',
afterEvent: (chart, args, opts) => {
@@ -54,17 +37,14 @@ const customEventListener = {
}
};
-Chart.defaults.color = colors.text;
-Chart.defaults.borderColor = colors.border;
+Chart.defaults.color = chartJsColors.text;
+Chart.defaults.borderColor = chartJsColors.border;
Chart.register(
TimeScale,
- CategoryScale,
LinearScale,
BarElement,
Title,
- Tooltip,
- Legend,
PointElement,
LineElement,
Filler,
@@ -122,7 +102,7 @@ export default {
do {
response = await GET(`${this.repoLink}/activity/contributors/data`);
if (response.status === 202) {
- await new Promise((resolve) => setTimeout(resolve, 1000)); // wait for 1 second before retrying
+ await sleep(1000); // wait for 1 second before retrying
}
} while (response.status === 202);
if (response.ok) {
@@ -222,7 +202,7 @@ export default {
pointRadius: 0,
pointHitRadius: 0,
fill: 'start',
- backgroundColor: colors[this.type],
+ backgroundColor: chartJsColors[this.type],
borderWidth: 0,
tension: 0.3,
},
@@ -254,7 +234,6 @@ export default {
title: {
display: type === 'main',
text: 'drag: zoom, shift+drag: pan, double click: reset zoom',
- color: colors.title,
position: 'top',
align: 'center',
},
@@ -262,9 +241,6 @@ export default {
chartType: type,
instance: this,
},
- legend: {
- display: false,
- },
zoom: {
pan: {
enabled: true,
diff --git a/web_src/js/features/code-frequency.js b/web_src/js/features/code-frequency.js
new file mode 100644
index 0000000000..103d82f6e3
--- /dev/null
+++ b/web_src/js/features/code-frequency.js
@@ -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');
+ }
+}
diff --git a/web_src/js/index.js b/web_src/js/index.js
index ddd435f05e..876e4291ee 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -87,6 +87,7 @@ import {onDomReady} from './utils/dom.js';
import {initRepoIssueList} from './features/repo-issue-list.js';
import {initCommonIssueListQuickGoto} from './features/common-issue-list.js';
import {initRepoContributors} from './features/contributors.js';
+import {initRepoCodeFrequency} from './features/code-frequency.js';
import {initRepoDiffCommitBranchesAndTags} from './features/repo-diff-commit.js';
import {initDirAuto} from './modules/dirauto.js';
@@ -177,6 +178,7 @@ onDomReady(() => {
initRepository();
initRepositoryActionView();
initRepoContributors();
+ initRepoCodeFrequency();
initCommitStatuses();
initCaptcha();
diff --git a/web_src/js/utils.js b/web_src/js/utils.js
index c82e42d349..3a2694335f 100644
--- a/web_src/js/utils.js
+++ b/web_src/js/utils.js
@@ -139,3 +139,5 @@ export function parseDom(text, contentType) {
export function serializeXml(node) {
return xmlSerializer.serializeToString(node);
}
+
+export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
diff --git a/web_src/js/utils/color.js b/web_src/js/utils/color.js
index 5d9c4ca45d..0ba6af49ee 100644
--- a/web_src/js/utils/color.js
+++ b/web_src/js/utils/color.js
@@ -19,3 +19,17 @@ function getLuminance(r, g, b) {
export function useLightTextOnBackground(r, g, b) {
return getLuminance(r, g, b) < 0.453;
}
+
+function resolveColors(obj) {
+ const styles = window.getComputedStyle(document.documentElement);
+ const getColor = (name) => styles.getPropertyValue(name).trim();
+ return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, getColor(value)]));
+}
+
+export const chartJsColors = resolveColors({
+ text: '--color-text',
+ border: '--color-secondary-alpha-60',
+ commits: '--color-primary-alpha-60',
+ additions: '--color-green',
+ deletions: '--color-red',
+});