gitea源码

RepoRecentCommits.vue 3.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. <script lang="ts" setup>
  2. import {SvgIcon} from '../svg.ts';
  3. import {
  4. Chart,
  5. Tooltip,
  6. BarElement,
  7. LinearScale,
  8. TimeScale,
  9. type ChartOptions,
  10. type ChartData,
  11. } from 'chart.js';
  12. import {GET} from '../modules/fetch.ts';
  13. import {Bar} from 'vue-chartjs';
  14. import {
  15. startDaysBetween,
  16. firstStartDateAfterDate,
  17. fillEmptyStartDaysWithZeroes,
  18. type DayData,
  19. type DayDataObject,
  20. } from '../utils/time.ts';
  21. import {chartJsColors} from '../utils/color.ts';
  22. import {sleep} from '../utils.ts';
  23. import 'chartjs-adapter-dayjs-4/dist/chartjs-adapter-dayjs-4.esm';
  24. import {onMounted, ref, shallowRef} from 'vue';
  25. const {pageData} = window.config;
  26. Chart.defaults.color = chartJsColors.text;
  27. Chart.defaults.borderColor = chartJsColors.border;
  28. Chart.register(
  29. TimeScale,
  30. LinearScale,
  31. BarElement,
  32. Tooltip,
  33. );
  34. defineProps<{
  35. locale: {
  36. loadingTitle: string;
  37. loadingTitleFailed: string;
  38. loadingInfo: string;
  39. };
  40. }>();
  41. const isLoading = shallowRef(false);
  42. const errorText = shallowRef('');
  43. const repoLink = pageData.repoLink;
  44. const data = ref<DayData[]>([]);
  45. onMounted(() => {
  46. fetchGraphData();
  47. });
  48. async function fetchGraphData() {
  49. isLoading.value = true;
  50. try {
  51. let response: Response;
  52. do {
  53. response = await GET(`${repoLink}/activity/recent-commits/data`);
  54. if (response.status === 202) {
  55. await sleep(1000); // wait for 1 second before retrying
  56. }
  57. } while (response.status === 202);
  58. if (response.ok) {
  59. const dayDataObj: DayDataObject = await response.json();
  60. const start = Object.values(dayDataObj)[0].week;
  61. const end = firstStartDateAfterDate(new Date());
  62. const startDays = startDaysBetween(start, end);
  63. data.value = fillEmptyStartDaysWithZeroes(startDays, dayDataObj).slice(-52);
  64. errorText.value = '';
  65. } else {
  66. errorText.value = response.statusText;
  67. }
  68. } catch (err) {
  69. errorText.value = err.message;
  70. } finally {
  71. isLoading.value = false;
  72. }
  73. }
  74. function toGraphData(data: DayData[]): ChartData<'bar'> {
  75. return {
  76. datasets: [
  77. {
  78. // @ts-expect-error -- bar chart expects one-dimensional data, but apparently x/y still works
  79. data: data.map((i) => ({x: i.week, y: i.commits})),
  80. label: 'Commits',
  81. backgroundColor: chartJsColors['commits'],
  82. borderWidth: 0,
  83. tension: 0.3,
  84. },
  85. ],
  86. };
  87. }
  88. const options: ChartOptions<'bar'> = {
  89. responsive: true,
  90. maintainAspectRatio: false,
  91. scales: {
  92. x: {
  93. type: 'time',
  94. grid: {
  95. display: false,
  96. },
  97. time: {
  98. minUnit: 'week',
  99. },
  100. ticks: {
  101. maxRotation: 0,
  102. maxTicksLimit: 52,
  103. },
  104. },
  105. y: {
  106. ticks: {
  107. maxTicksLimit: 6,
  108. },
  109. },
  110. },
  111. } satisfies ChartOptions;
  112. </script>
  113. <template>
  114. <div>
  115. <div class="ui header tw-flex tw-items-center tw-justify-between">
  116. {{ isLoading ? locale.loadingTitle : errorText ? locale.loadingTitleFailed: "Number of commits in the past year" }}
  117. </div>
  118. <div class="tw-flex ui segment main-graph">
  119. <div v-if="isLoading || errorText !== ''" class="gt-tc tw-m-auto">
  120. <div v-if="isLoading">
  121. <SvgIcon name="octicon-sync" class="tw-mr-2 circular-spin"/>
  122. {{ locale.loadingInfo }}
  123. </div>
  124. <div v-else class="text red">
  125. <SvgIcon name="octicon-x-circle-fill"/>
  126. {{ errorText }}
  127. </div>
  128. </div>
  129. <Bar
  130. v-memo="data" v-if="data.length !== 0"
  131. :data="toGraphData(data)" :options="options"
  132. />
  133. </div>
  134. </div>
  135. </template>
  136. <style scoped>
  137. .main-graph {
  138. height: 250px;
  139. }
  140. </style>