code.gitea.io/gitea@v1.22.3/web_src/js/components/RepoCodeFrequency.vue (about) 1 <script> 2 import {SvgIcon} from '../svg.js'; 3 import { 4 Chart, 5 Legend, 6 LinearScale, 7 TimeScale, 8 PointElement, 9 LineElement, 10 Filler, 11 } from 'chart.js'; 12 import {GET} from '../modules/fetch.js'; 13 import {Line as ChartLine} from 'vue-chartjs'; 14 import { 15 startDaysBetween, 16 firstStartDateAfterDate, 17 fillEmptyStartDaysWithZeroes, 18 } from '../utils/time.js'; 19 import {chartJsColors} from '../utils/color.js'; 20 import {sleep} from '../utils.js'; 21 import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm'; 22 23 const {pageData} = window.config; 24 25 Chart.defaults.color = chartJsColors.text; 26 Chart.defaults.borderColor = chartJsColors.border; 27 28 Chart.register( 29 TimeScale, 30 LinearScale, 31 Legend, 32 PointElement, 33 LineElement, 34 Filler, 35 ); 36 37 export default { 38 components: {ChartLine, SvgIcon}, 39 props: { 40 locale: { 41 type: Object, 42 required: true, 43 }, 44 }, 45 data: () => ({ 46 isLoading: false, 47 errorText: '', 48 repoLink: pageData.repoLink || [], 49 data: [], 50 }), 51 mounted() { 52 this.fetchGraphData(); 53 }, 54 methods: { 55 async fetchGraphData() { 56 this.isLoading = true; 57 try { 58 let response; 59 do { 60 response = await GET(`${this.repoLink}/activity/code-frequency/data`); 61 if (response.status === 202) { 62 await sleep(1000); // wait for 1 second before retrying 63 } 64 } while (response.status === 202); 65 if (response.ok) { 66 this.data = await response.json(); 67 const weekValues = Object.values(this.data); 68 const start = weekValues[0].week; 69 const end = firstStartDateAfterDate(new Date()); 70 const startDays = startDaysBetween(start, end); 71 this.data = fillEmptyStartDaysWithZeroes(startDays, this.data); 72 this.errorText = ''; 73 } else { 74 this.errorText = response.statusText; 75 } 76 } catch (err) { 77 this.errorText = err.message; 78 } finally { 79 this.isLoading = false; 80 } 81 }, 82 83 toGraphData(data) { 84 return { 85 datasets: [ 86 { 87 data: data.map((i) => ({x: i.week, y: i.additions})), 88 pointRadius: 0, 89 pointHitRadius: 0, 90 fill: true, 91 label: 'Additions', 92 backgroundColor: chartJsColors['additions'], 93 borderWidth: 0, 94 tension: 0.3, 95 }, 96 { 97 data: data.map((i) => ({x: i.week, y: -i.deletions})), 98 pointRadius: 0, 99 pointHitRadius: 0, 100 fill: true, 101 label: 'Deletions', 102 backgroundColor: chartJsColors['deletions'], 103 borderWidth: 0, 104 tension: 0.3, 105 }, 106 ], 107 }; 108 }, 109 110 getOptions() { 111 return { 112 responsive: true, 113 maintainAspectRatio: false, 114 animation: true, 115 plugins: { 116 legend: { 117 display: true, 118 }, 119 }, 120 scales: { 121 x: { 122 type: 'time', 123 grid: { 124 display: false, 125 }, 126 time: { 127 minUnit: 'month', 128 }, 129 ticks: { 130 maxRotation: 0, 131 maxTicksLimit: 12, 132 }, 133 }, 134 y: { 135 ticks: { 136 maxTicksLimit: 6, 137 }, 138 }, 139 }, 140 }; 141 }, 142 }, 143 }; 144 </script> 145 <template> 146 <div> 147 <div class="ui header tw-flex tw-items-center tw-justify-between"> 148 {{ isLoading ? locale.loadingTitle : errorText ? locale.loadingTitleFailed: `Code frequency over the history of ${repoLink.slice(1)}` }} 149 </div> 150 <div class="tw-flex ui segment main-graph"> 151 <div v-if="isLoading || errorText !== ''" class="gt-tc tw-m-auto"> 152 <div v-if="isLoading"> 153 <SvgIcon name="octicon-sync" class="tw-mr-2 job-status-rotate"/> 154 {{ locale.loadingInfo }} 155 </div> 156 <div v-else class="text red"> 157 <SvgIcon name="octicon-x-circle-fill"/> 158 {{ errorText }} 159 </div> 160 </div> 161 <ChartLine 162 v-memo="data" v-if="data.length !== 0" 163 :data="toGraphData(data)" :options="getOptions()" 164 /> 165 </div> 166 </div> 167 </template> 168 <style scoped> 169 .main-graph { 170 height: 440px; 171 } 172 </style>