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.

ProjectCard.tsx 9.9KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291
  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 { Link, LinkStandalone } from '@sonarsource/echoes-react';
  22. import classNames from 'classnames';
  23. import {
  24. Badge,
  25. Card,
  26. LightLabel,
  27. LightPrimary,
  28. Note,
  29. QualityGateIndicator,
  30. SeparatorCircleIcon,
  31. SubnavigationFlowSeparator,
  32. Tags,
  33. themeBorder,
  34. themeColor,
  35. } from 'design-system';
  36. import { isEmpty } from 'lodash';
  37. import * as React from 'react';
  38. import { FormattedMessage } from 'react-intl';
  39. import Favorite from '../../../../components/controls/Favorite';
  40. import Tooltip from '../../../../components/controls/Tooltip';
  41. import DateFromNow from '../../../../components/intl/DateFromNow';
  42. import DateTimeFormatter from '../../../../components/intl/DateTimeFormatter';
  43. import Measure from '../../../../components/measure/Measure';
  44. import { translate, translateWithParameters } from '../../../../helpers/l10n';
  45. import { formatMeasure } from '../../../../helpers/measures';
  46. import { isDefined } from '../../../../helpers/types';
  47. import { getProjectUrl } from '../../../../helpers/urls';
  48. import { ComponentQualifier } from '../../../../types/component';
  49. import { MetricKey, MetricType } from '../../../../types/metrics';
  50. import { Status } from '../../../../types/types';
  51. import { CurrentUser, isLoggedIn } from '../../../../types/users';
  52. import { Project } from '../../types';
  53. import ProjectCardLanguages from './ProjectCardLanguages';
  54. import ProjectCardMeasures from './ProjectCardMeasures';
  55. interface Props {
  56. currentUser: CurrentUser;
  57. handleFavorite: (component: string, isFavorite: boolean) => void;
  58. project: Project;
  59. type?: string;
  60. }
  61. function renderFirstLine(
  62. project: Props['project'],
  63. handleFavorite: Props['handleFavorite'],
  64. isNewCode: boolean,
  65. ) {
  66. const { analysisDate, isFavorite, key, measures, name, qualifier, tags, visibility } = project;
  67. const awaitingScan =
  68. [
  69. MetricKey.reliability_issues,
  70. MetricKey.maintainability_issues,
  71. MetricKey.security_issues,
  72. ].every((key) => measures[key] === undefined) &&
  73. !isNewCode &&
  74. !isEmpty(analysisDate) &&
  75. measures.ncloc !== undefined;
  76. const formatted = formatMeasure(measures[MetricKey.alert_status], MetricType.Level);
  77. const qualityGateLabel = translateWithParameters('overview.quality_gate_x', formatted);
  78. return (
  79. <>
  80. <div className="sw-flex sw-justify-between sw-items-center ">
  81. <div className="sw-flex sw-items-center ">
  82. {isDefined(isFavorite) && (
  83. <Favorite
  84. className="sw-mr-2"
  85. component={key}
  86. componentName={name}
  87. favorite={isFavorite}
  88. handleFavorite={handleFavorite}
  89. qualifier={qualifier}
  90. />
  91. )}
  92. <span className="it__project-card-name" title={name}>
  93. <LinkStandalone to={getProjectUrl(key)}>{name}</LinkStandalone>
  94. </span>
  95. {qualifier === ComponentQualifier.Application && (
  96. <Tooltip
  97. overlay={
  98. <span>
  99. {translate('qualifier.APP')}
  100. {measures.projects !== '' && (
  101. <span>
  102. {' ‒ '}
  103. {translateWithParameters('x_projects_', measures.projects)}
  104. </span>
  105. )}
  106. </span>
  107. }
  108. >
  109. <span>
  110. <Badge className="sw-ml-2">{translate('qualifier.APP')}</Badge>
  111. </span>
  112. </Tooltip>
  113. )}
  114. <Tooltip overlay={translate('visibility', visibility, 'description', qualifier)}>
  115. <span>
  116. <Badge className="sw-ml-2">{translate('visibility', visibility)}</Badge>
  117. </span>
  118. </Tooltip>
  119. {awaitingScan && !isNewCode && !isEmpty(analysisDate) && measures.ncloc !== undefined && (
  120. <Tooltip overlay={translate(`projects.awaiting_scan.description.${qualifier}`)}>
  121. <span>
  122. <Badge variant="new" className="sw-ml-2">
  123. {translate('projects.awaiting_scan')}
  124. </Badge>
  125. </span>
  126. </Tooltip>
  127. )}
  128. </div>
  129. {isDefined(analysisDate) && analysisDate !== '' && (
  130. <Tooltip overlay={qualityGateLabel}>
  131. <span className="sw-flex sw-items-center">
  132. <QualityGateIndicator
  133. status={(measures[MetricKey.alert_status] as Status) ?? 'NONE'}
  134. ariaLabel={qualityGateLabel}
  135. />
  136. <LightPrimary className="sw-ml-2 sw-body-sm-highlight">{formatted}</LightPrimary>
  137. </span>
  138. </Tooltip>
  139. )}
  140. </div>
  141. <LightLabel as="div" className="sw-flex sw-items-center sw-mt-3">
  142. {isDefined(analysisDate) && analysisDate !== '' && (
  143. <DateTimeFormatter date={analysisDate}>
  144. {(formattedAnalysisDate) => (
  145. <span className="sw-body-sm-highlight" title={formattedAnalysisDate}>
  146. <FormattedMessage
  147. id="projects.last_analysis_on_x"
  148. defaultMessage={translate('projects.last_analysis_on_x')}
  149. values={{
  150. date: <DateFromNow className="sw-body-sm" date={analysisDate} />,
  151. }}
  152. />
  153. </span>
  154. )}
  155. </DateTimeFormatter>
  156. )}
  157. {isNewCode
  158. ? measures[MetricKey.new_lines] != null && (
  159. <>
  160. <SeparatorCircleIcon className="sw-mx-1" />
  161. <div>
  162. <span className="sw-body-sm-highlight sw-mr-1" data-key={MetricKey.new_lines}>
  163. <Measure
  164. metricKey={MetricKey.new_lines}
  165. metricType={MetricType.ShortInteger}
  166. value={measures.new_lines}
  167. />
  168. </span>
  169. <span className="sw-body-sm">{translate('metric.new_lines.name')}</span>
  170. </div>
  171. </>
  172. )
  173. : measures[MetricKey.ncloc] != null && (
  174. <>
  175. <SeparatorCircleIcon className="sw-mx-1" />
  176. <div>
  177. <span className="sw-body-sm-highlight sw-mr-1" data-key={MetricKey.ncloc}>
  178. <Measure
  179. metricKey={MetricKey.ncloc}
  180. metricType={MetricType.ShortInteger}
  181. value={measures.ncloc}
  182. />
  183. </span>
  184. <span className="sw-body-sm">{translate('metric.ncloc.name')}</span>
  185. </div>
  186. <SeparatorCircleIcon className="sw-mx-1" />
  187. <span className="sw-body-sm" data-key={MetricKey.ncloc_language_distribution}>
  188. <ProjectCardLanguages distribution={measures.ncloc_language_distribution} />
  189. </span>
  190. </>
  191. )}
  192. {tags.length > 0 && (
  193. <>
  194. <SeparatorCircleIcon className="sw-mx-1" />
  195. <Tags
  196. className="sw-body-sm"
  197. emptyText={translate('issue.no_tag')}
  198. ariaTagsListLabel={translate('issue.tags')}
  199. tooltip={Tooltip}
  200. tags={tags}
  201. tagsToDisplay={2}
  202. />
  203. </>
  204. )}
  205. </LightLabel>
  206. </>
  207. );
  208. }
  209. function renderSecondLine(
  210. currentUser: Props['currentUser'],
  211. project: Props['project'],
  212. isNewCode: boolean,
  213. ) {
  214. const { analysisDate, key, leakPeriodDate, measures, qualifier, isScannable } = project;
  215. if (!isEmpty(analysisDate) && (!isNewCode || !isEmpty(leakPeriodDate))) {
  216. return (
  217. <ProjectCardMeasures
  218. measures={measures}
  219. componentQualifier={qualifier}
  220. isNewCode={isNewCode}
  221. />
  222. );
  223. }
  224. return (
  225. <div className="sw-flex sw-items-center">
  226. <Note className="sw-py-4">
  227. {isNewCode && analysisDate
  228. ? translate('projects.no_new_code_period', qualifier)
  229. : translate('projects.not_analyzed', qualifier)}
  230. </Note>
  231. {qualifier !== ComponentQualifier.Application &&
  232. isEmpty(analysisDate) &&
  233. isLoggedIn(currentUser) &&
  234. isScannable && (
  235. <Link className="sw-ml-2 sw-body-sm-highlight" to={getProjectUrl(key)}>
  236. {translate('projects.configure_analysis')}
  237. </Link>
  238. )}
  239. </div>
  240. );
  241. }
  242. export default function ProjectCard(props: Readonly<Props>) {
  243. const { currentUser, type, project } = props;
  244. const isNewCode = type === 'leak';
  245. return (
  246. <ProjectCardWrapper
  247. className={classNames(
  248. 'it_project_card sw-relative sw-box-border sw-rounded-1 sw-mb-page sw-h-full',
  249. )}
  250. data-key={project.key}
  251. >
  252. {renderFirstLine(project, props.handleFavorite, isNewCode)}
  253. <SubnavigationFlowSeparator className="sw-my-3" />
  254. {renderSecondLine(currentUser, project, isNewCode)}
  255. </ProjectCardWrapper>
  256. );
  257. }
  258. const ProjectCardWrapper = styled(Card)`
  259. background-color: ${themeColor('projectCardBackground')};
  260. border: ${themeBorder('default', 'projectCardBorder')};
  261. &.project-card-disabled *:not(g):not(path) {
  262. color: ${themeColor('projectCardDisabled')} !important;
  263. }
  264. `;