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.

ProjectActivityAnalysis.tsx 8.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  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 {
  23. ActionsDropdown,
  24. HelperHintIcon,
  25. ItemButton,
  26. ItemDangerButton,
  27. ItemDivider,
  28. PopupZLevel,
  29. themeBorder,
  30. themeColor,
  31. } from 'design-system';
  32. import * as React from 'react';
  33. import { WrappedComponentProps, injectIntl } from 'react-intl';
  34. import ClickEventBoundary from '../../../components/controls/ClickEventBoundary';
  35. import Tooltip from '../../../components/controls/Tooltip';
  36. import { formatterOption } from '../../../components/intl/DateTimeFormatter';
  37. import TimeFormatter from '../../../components/intl/TimeFormatter';
  38. import { parseDate } from '../../../helpers/dates';
  39. import { translate, translateWithParameters } from '../../../helpers/l10n';
  40. import { ParsedAnalysis, ProjectAnalysisEventCategory } from '../../../types/project-activity';
  41. import Events from './Events';
  42. import AddEventForm from './forms/AddEventForm';
  43. import RemoveAnalysisForm from './forms/RemoveAnalysisForm';
  44. export interface ProjectActivityAnalysisProps extends WrappedComponentProps {
  45. analysis: ParsedAnalysis;
  46. canAdmin?: boolean;
  47. canDeleteAnalyses?: boolean;
  48. canCreateVersion: boolean;
  49. isBaseline: boolean;
  50. isFirst: boolean;
  51. selected: boolean;
  52. onUpdateSelectedDate: (date: Date) => void;
  53. }
  54. export enum Dialog {
  55. AddEvent = 'add_event',
  56. AddVersion = 'add_version',
  57. RemoveAnalysis = 'remove_analysis',
  58. }
  59. function ProjectActivityAnalysis(props: ProjectActivityAnalysisProps) {
  60. let node: HTMLLIElement | null = null;
  61. const {
  62. analysis,
  63. isBaseline,
  64. isFirst,
  65. canAdmin,
  66. canCreateVersion,
  67. selected,
  68. intl: { formatDate },
  69. } = props;
  70. React.useEffect(() => {
  71. if (node && selected) {
  72. node.scrollIntoView({ behavior: 'smooth', block: 'center' });
  73. }
  74. });
  75. const [dialog, setDialog] = React.useState<Dialog | undefined>();
  76. const closeDialog = () => setDialog(undefined);
  77. const parsedDate = parseDate(analysis.date);
  78. const hasVersion = analysis.events.find((event) => event.category === 'VERSION') != null;
  79. const canAddVersion = canAdmin && !hasVersion && canCreateVersion;
  80. const canAddEvent = canAdmin;
  81. const canDeleteAnalyses =
  82. props.canDeleteAnalyses && !isFirst && !analysis.manualNewCodePeriodBaseline;
  83. let tooltipContent = <TimeFormatter date={parsedDate} long />;
  84. if (analysis.buildString) {
  85. tooltipContent = (
  86. <>
  87. {tooltipContent}{' '}
  88. {translateWithParameters('project_activity.analysis_build_string_X', analysis.buildString)}
  89. </>
  90. );
  91. }
  92. return (
  93. <>
  94. <Tooltip mouseEnterDelay={0.5} overlay={tooltipContent} placement="left">
  95. <ActivityAnalysisListItem
  96. className={classNames(
  97. 'it__project-activity-analysis sw-flex sw-cursor-pointer sw-p-1 sw-relative',
  98. {
  99. active: selected,
  100. },
  101. )}
  102. aria-label={translateWithParameters(
  103. 'project_activity.show_analysis_X_on_graph',
  104. analysis.buildString ?? formatDate(parsedDate, formatterOption),
  105. )}
  106. onClick={() => {
  107. if (!selected) {
  108. props.onUpdateSelectedDate(analysis.date);
  109. }
  110. }}
  111. ref={(ref) => (node = ref)}
  112. >
  113. <div className="it__project-activity-time">
  114. <ActivityTime className="sw-h-page sw-body-sm-highlight sw-text-right sw-mr-2 sw-py-1/2">
  115. <TimeFormatter date={parsedDate} long={false}>
  116. {(formattedTime) => (
  117. <time dateTime={parsedDate.toISOString()}>{formattedTime}</time>
  118. )}
  119. </TimeFormatter>
  120. </ActivityTime>
  121. </div>
  122. {(canAddVersion || canAddEvent || canDeleteAnalyses) && (
  123. <ClickEventBoundary>
  124. <div className="sw-h-page sw-grow-0 sw-shrink-0 sw-mr-4 sw-relative">
  125. <ActionsDropdown
  126. ariaLabel={translateWithParameters(
  127. 'project_activity.analysis_X_actions',
  128. analysis.buildString ?? formatDate(parsedDate, formatterOption),
  129. )}
  130. buttonSize="small"
  131. id="it__analysis-actions"
  132. zLevel={PopupZLevel.Absolute}
  133. >
  134. {canAddVersion && (
  135. <ItemButton
  136. className="js-add-version"
  137. onClick={() => setDialog(Dialog.AddVersion)}
  138. >
  139. {translate('project_activity.add_version')}
  140. </ItemButton>
  141. )}
  142. {canAddEvent && (
  143. <ItemButton className="js-add-event" onClick={() => setDialog(Dialog.AddEvent)}>
  144. {translate('project_activity.add_custom_event')}
  145. </ItemButton>
  146. )}
  147. {(canAddVersion || canAddEvent) && canDeleteAnalyses && <ItemDivider />}
  148. {canDeleteAnalyses && (
  149. <ItemDangerButton
  150. className="js-delete-analysis"
  151. onClick={() => setDialog(Dialog.RemoveAnalysis)}
  152. >
  153. {translate('project_activity.delete_analysis')}
  154. </ItemDangerButton>
  155. )}
  156. </ActionsDropdown>
  157. {[Dialog.AddEvent, Dialog.AddVersion].includes(dialog as Dialog) && (
  158. <AddEventForm
  159. category={
  160. dialog === Dialog.AddVersion
  161. ? ProjectAnalysisEventCategory.Version
  162. : undefined
  163. }
  164. addEventButtonText={
  165. dialog === Dialog.AddVersion
  166. ? 'project_activity.add_version'
  167. : 'project_activity.add_custom_event'
  168. }
  169. analysis={analysis}
  170. onClose={closeDialog}
  171. />
  172. )}
  173. {dialog === 'remove_analysis' && (
  174. <RemoveAnalysisForm analysis={analysis} onClose={closeDialog} />
  175. )}
  176. </div>
  177. </ClickEventBoundary>
  178. )}
  179. {analysis.events.length > 0 && (
  180. <Events
  181. analysisKey={analysis.key}
  182. canAdmin={canAdmin}
  183. events={analysis.events}
  184. isFirst={isFirst}
  185. />
  186. )}
  187. </ActivityAnalysisListItem>
  188. </Tooltip>
  189. {isBaseline && (
  190. <BaselineMarker className="sw-body-sm sw-mt-2">
  191. <span className="sw-py-1/2 sw-px-1">
  192. {translate('project_activity.new_code_period_start')}
  193. </span>
  194. <Tooltip
  195. overlay={translate('project_activity.new_code_period_start.help')}
  196. placement="top"
  197. >
  198. <HelperHintIcon className="sw-ml-1" />
  199. </Tooltip>
  200. </BaselineMarker>
  201. )}
  202. </>
  203. );
  204. }
  205. const ActivityTime = styled.div`
  206. box-sizing: border-box;
  207. width: 4.5rem;
  208. `;
  209. const ActivityAnalysisListItem = styled.li`
  210. border-bottom: ${themeBorder('default')};
  211. border-left: ${themeBorder('active', 'transparent')};
  212. &:first-of-type {
  213. border-top: ${themeBorder('default')};
  214. }
  215. &:focus {
  216. outline: none;
  217. }
  218. &:hover,
  219. &:focus,
  220. &.active {
  221. background-color: ${themeColor('subnavigationHover')};
  222. }
  223. &.active {
  224. border-left: ${themeBorder('active')};
  225. }
  226. `;
  227. export const BaselineMarker = styled.li`
  228. display: flex;
  229. align-items: center;
  230. border-bottom: ${themeBorder('default', 'newCodeHighlight')};
  231. & span {
  232. background-color: ${themeColor('dropdownMenuFocus')};
  233. }
  234. `;
  235. export default injectIntl(ProjectActivityAnalysis);