You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ProjectActivityAnalysesList.tsx 8.0KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2024 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. import styled from '@emotion/styled';
  21. import classNames from 'classnames';
  22. import { isEqual } from 'date-fns';
  23. import { Badge, HelperHintIcon, LightLabel, Spinner, themeColor } from 'design-system';
  24. import * as React from 'react';
  25. import Tooltip from '../../../components/controls/Tooltip';
  26. import DateFormatter from '../../../components/intl/DateFormatter';
  27. import { toShortISO8601String } from '../../../helpers/dates';
  28. import { translate } from '../../../helpers/l10n';
  29. import { ComponentQualifier } from '../../../types/component';
  30. import { ParsedAnalysis } from '../../../types/project-activity';
  31. import { AnalysesByDay, Query, activityQueryChanged, getAnalysesByVersionByDay } from '../utils';
  32. import ProjectActivityAnalysis, { BaselineMarker } from './ProjectActivityAnalysis';
  33. interface Props {
  34. analyses: ParsedAnalysis[];
  35. analysesLoading: boolean;
  36. canAdmin?: boolean;
  37. canDeleteAnalyses?: boolean;
  38. initializing: boolean;
  39. leakPeriodDate?: Date;
  40. project: { qualifier: string };
  41. query: Query;
  42. onUpdateQuery: (changes: Partial<Query>) => void;
  43. }
  44. const LIST_MARGIN_TOP = 24;
  45. export default class ProjectActivityAnalysesList extends React.PureComponent<Props> {
  46. scrollContainer?: HTMLUListElement | null;
  47. componentDidUpdate(prevProps: Props) {
  48. const selectedDate = this.props.query.selectedDate
  49. ? this.props.query.selectedDate.valueOf()
  50. : null;
  51. if (
  52. this.scrollContainer &&
  53. activityQueryChanged(prevProps.query, this.props.query) &&
  54. !this.props.analyses.some(({ date }) => date.valueOf() === selectedDate)
  55. ) {
  56. this.scrollContainer.scrollTop = 0;
  57. }
  58. }
  59. handleUpdateSelectedDate = (date: Date) => {
  60. this.props.onUpdateQuery({ selectedDate: date });
  61. };
  62. getNewCodePeriodStartKey(versionByDay: AnalysesByDay[]): {
  63. firstNewCodeAnalysisKey: string | undefined;
  64. baselineAnalysisKey: string | undefined;
  65. } {
  66. const { leakPeriodDate } = this.props;
  67. if (!leakPeriodDate) {
  68. return { firstNewCodeAnalysisKey: undefined, baselineAnalysisKey: undefined };
  69. }
  70. // In response, the first new code analysis comes before the baseline analysis
  71. // This variable is to track the previous analysis and return when next is baseline analysis
  72. let prevAnalysis;
  73. for (const version of versionByDay) {
  74. const days = Object.keys(version.byDay);
  75. for (const day of days) {
  76. for (const analysis of version.byDay[day]) {
  77. if (isEqual(leakPeriodDate, analysis.date)) {
  78. return {
  79. firstNewCodeAnalysisKey: prevAnalysis?.key,
  80. baselineAnalysisKey: analysis.key,
  81. };
  82. }
  83. prevAnalysis = analysis;
  84. }
  85. }
  86. }
  87. return { firstNewCodeAnalysisKey: undefined, baselineAnalysisKey: undefined };
  88. }
  89. renderAnalysis(analysis: ParsedAnalysis, newCodeKey?: string) {
  90. const firstAnalysisKey = this.props.analyses[0].key;
  91. const selectedDate = this.props.query.selectedDate
  92. ? this.props.query.selectedDate.valueOf()
  93. : null;
  94. return (
  95. <ProjectActivityAnalysis
  96. analysis={analysis}
  97. canAdmin={this.props.canAdmin}
  98. canCreateVersion={this.props.project.qualifier === ComponentQualifier.Project}
  99. canDeleteAnalyses={this.props.canDeleteAnalyses}
  100. isBaseline={analysis.key === newCodeKey}
  101. isFirst={analysis.key === firstAnalysisKey}
  102. key={analysis.key}
  103. selected={analysis.date.valueOf() === selectedDate}
  104. onUpdateSelectedDate={this.handleUpdateSelectedDate}
  105. />
  106. );
  107. }
  108. render() {
  109. const byVersionByDay = getAnalysesByVersionByDay(this.props.analyses, this.props.query);
  110. const newCodePeriod = this.getNewCodePeriodStartKey(byVersionByDay);
  111. const hasFilteredData =
  112. byVersionByDay.length > 1 ||
  113. (byVersionByDay.length === 1 && Object.keys(byVersionByDay[0].byDay).length > 0);
  114. if (this.props.analyses.length === 0 || !hasFilteredData) {
  115. return (
  116. <div>
  117. {this.props.initializing ? (
  118. <div className="sw-p-4 sw-body-sm">
  119. <Spinner />
  120. </div>
  121. ) : (
  122. <div className="sw-p-4 sw-body-sm">
  123. <LightLabel>{translate('no_results')}</LightLabel>
  124. </div>
  125. )}
  126. </div>
  127. );
  128. }
  129. return (
  130. <ul
  131. className="it__project-activity-versions-list sw-box-border sw-overflow-auto sw-grow sw-shrink-0 sw-py-0 sw-px-4"
  132. ref={(element) => (this.scrollContainer = element)}
  133. style={{
  134. height: 'calc(100vh - 250px)',
  135. marginTop:
  136. this.props.project.qualifier === ComponentQualifier.Project
  137. ? LIST_MARGIN_TOP
  138. : undefined,
  139. }}
  140. >
  141. {newCodePeriod.baselineAnalysisKey !== undefined &&
  142. newCodePeriod.firstNewCodeAnalysisKey === undefined && (
  143. <BaselineMarker className="sw-body-sm sw-mb-2">
  144. <span className="sw-py-1/2 sw-px-1">
  145. {translate('project_activity.new_code_period_start')}
  146. </span>
  147. <Tooltip
  148. overlay={translate('project_activity.new_code_period_start.help')}
  149. placement="top"
  150. >
  151. <HelperHintIcon className="sw-ml-1" />
  152. </Tooltip>
  153. </BaselineMarker>
  154. )}
  155. {byVersionByDay.map((version, idx) => {
  156. const days = Object.keys(version.byDay);
  157. if (days.length <= 0) {
  158. return null;
  159. }
  160. return (
  161. <li key={version.key || 'noversion'}>
  162. {version.version && (
  163. <VersionTagStyled
  164. className={classNames(
  165. 'sw-sticky sw-top-0 sw-left-0 sw-pb-1 -sw-ml-4 sw-z-normal',
  166. {
  167. 'sw-top-0 sw-pt-0': idx === 0,
  168. },
  169. )}
  170. >
  171. <Tooltip
  172. mouseEnterDelay={0.5}
  173. overlay={`${translate('version')} ${version.version}`}
  174. >
  175. <Badge className="sw-p-1">{version.version}</Badge>
  176. </Tooltip>
  177. </VersionTagStyled>
  178. )}
  179. <ul className="it__project-activity-days-list">
  180. {days.map((day) => (
  181. <li
  182. className="it__project-activity-day sw-mt-1 sw-mb-4"
  183. data-day={toShortISO8601String(Number(day))}
  184. key={day}
  185. >
  186. <div className="sw-body-md-highlight sw-mb-3">
  187. <DateFormatter date={Number(day)} long />
  188. </div>
  189. <ul className="it__project-activity-analyses-list">
  190. {version.byDay[day]?.map((analysis) =>
  191. this.renderAnalysis(analysis, newCodePeriod.firstNewCodeAnalysisKey),
  192. )}
  193. </ul>
  194. </li>
  195. ))}
  196. </ul>
  197. </li>
  198. );
  199. })}
  200. {this.props.analysesLoading && (
  201. <li className="sw-text-center">
  202. <Spinner />
  203. </li>
  204. )}
  205. </ul>
  206. );
  207. }
  208. }
  209. const VersionTagStyled = styled.div`
  210. background-color: ${themeColor('backgroundSecondary')};
  211. `;