- /*
- * SonarQube
- * Copyright (C) 2009-2024 SonarSource SA
- * mailto:info AT sonarsource DOT com
- *
- * This program is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 3 of the License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public License
- * along with this program; if not, write to the Free Software Foundation,
- * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
- */
-
- import styled from '@emotion/styled';
- import { Link, LinkStandalone } from '@sonarsource/echoes-react';
- import classNames from 'classnames';
- import {
- Badge,
- Card,
- LightLabel,
- LightPrimary,
- Note,
- QualityGateIndicator,
- SeparatorCircleIcon,
- SubnavigationFlowSeparator,
- Tags,
- themeBorder,
- themeColor,
- } from 'design-system';
- import { isEmpty } from 'lodash';
- import * as React from 'react';
- import { FormattedMessage } from 'react-intl';
- import Favorite from '../../../../components/controls/Favorite';
- import Tooltip from '../../../../components/controls/Tooltip';
- import DateFromNow from '../../../../components/intl/DateFromNow';
- import DateTimeFormatter from '../../../../components/intl/DateTimeFormatter';
- import Measure from '../../../../components/measure/Measure';
- import { translate, translateWithParameters } from '../../../../helpers/l10n';
- import { formatMeasure } from '../../../../helpers/measures';
- import { isDefined } from '../../../../helpers/types';
- import { getProjectUrl } from '../../../../helpers/urls';
- import { ComponentQualifier } from '../../../../types/component';
- import { MetricKey, MetricType } from '../../../../types/metrics';
- import { Status } from '../../../../types/types';
- import { CurrentUser, isLoggedIn } from '../../../../types/users';
- import { Project } from '../../types';
- import ProjectCardLanguages from './ProjectCardLanguages';
- import ProjectCardMeasures from './ProjectCardMeasures';
-
- interface Props {
- currentUser: CurrentUser;
- handleFavorite: (component: string, isFavorite: boolean) => void;
- project: Project;
- type?: string;
- }
-
- function renderFirstLine(
- project: Props['project'],
- handleFavorite: Props['handleFavorite'],
- isNewCode: boolean,
- ) {
- const { analysisDate, isFavorite, key, measures, name, qualifier, tags, visibility } = project;
- const awaitingScan =
- [
- MetricKey.reliability_issues,
- MetricKey.maintainability_issues,
- MetricKey.security_issues,
- ].every((key) => measures[key] === undefined) &&
- !isNewCode &&
- !isEmpty(analysisDate) &&
- measures.ncloc !== undefined;
- const formatted = formatMeasure(measures[MetricKey.alert_status], MetricType.Level);
- const qualityGateLabel = translateWithParameters('overview.quality_gate_x', formatted);
- return (
- <>
- <div className="sw-flex sw-justify-between sw-items-center ">
- <div className="sw-flex sw-items-center ">
- {isDefined(isFavorite) && (
- <Favorite
- className="sw-mr-2"
- component={key}
- componentName={name}
- favorite={isFavorite}
- handleFavorite={handleFavorite}
- qualifier={qualifier}
- />
- )}
-
- <span className="it__project-card-name" title={name}>
- <LinkStandalone to={getProjectUrl(key)}>{name}</LinkStandalone>
- </span>
-
- {qualifier === ComponentQualifier.Application && (
- <Tooltip
- overlay={
- <span>
- {translate('qualifier.APP')}
- {measures.projects !== '' && (
- <span>
- {' ‒ '}
- {translateWithParameters('x_projects_', measures.projects)}
- </span>
- )}
- </span>
- }
- >
- <span>
- <Badge className="sw-ml-2">{translate('qualifier.APP')}</Badge>
- </span>
- </Tooltip>
- )}
-
- <Tooltip overlay={translate('visibility', visibility, 'description', qualifier)}>
- <span>
- <Badge className="sw-ml-2">{translate('visibility', visibility)}</Badge>
- </span>
- </Tooltip>
-
- {awaitingScan && !isNewCode && !isEmpty(analysisDate) && measures.ncloc !== undefined && (
- <Tooltip overlay={translate(`projects.awaiting_scan.description.${qualifier}`)}>
- <span>
- <Badge variant="new" className="sw-ml-2">
- {translate('projects.awaiting_scan')}
- </Badge>
- </span>
- </Tooltip>
- )}
- </div>
-
- {isDefined(analysisDate) && analysisDate !== '' && (
- <Tooltip overlay={qualityGateLabel}>
- <span className="sw-flex sw-items-center">
- <QualityGateIndicator
- status={(measures[MetricKey.alert_status] as Status) ?? 'NONE'}
- ariaLabel={qualityGateLabel}
- />
- <LightPrimary className="sw-ml-2 sw-body-sm-highlight">{formatted}</LightPrimary>
- </span>
- </Tooltip>
- )}
- </div>
-
- <LightLabel as="div" className="sw-flex sw-items-center sw-mt-3">
- {isDefined(analysisDate) && analysisDate !== '' && (
- <DateTimeFormatter date={analysisDate}>
- {(formattedAnalysisDate) => (
- <span className="sw-body-sm-highlight" title={formattedAnalysisDate}>
- <FormattedMessage
- id="projects.last_analysis_on_x"
- defaultMessage={translate('projects.last_analysis_on_x')}
- values={{
- date: <DateFromNow className="sw-body-sm" date={analysisDate} />,
- }}
- />
- </span>
- )}
- </DateTimeFormatter>
- )}
-
- {isNewCode
- ? measures[MetricKey.new_lines] != null && (
- <>
- <SeparatorCircleIcon className="sw-mx-1" />
-
- <div>
- <span className="sw-body-sm-highlight sw-mr-1" data-key={MetricKey.new_lines}>
- <Measure
- metricKey={MetricKey.new_lines}
- metricType={MetricType.ShortInteger}
- value={measures.new_lines}
- />
- </span>
-
- <span className="sw-body-sm">{translate('metric.new_lines.name')}</span>
- </div>
- </>
- )
- : measures[MetricKey.ncloc] != null && (
- <>
- <SeparatorCircleIcon className="sw-mx-1" />
-
- <div>
- <span className="sw-body-sm-highlight sw-mr-1" data-key={MetricKey.ncloc}>
- <Measure
- metricKey={MetricKey.ncloc}
- metricType={MetricType.ShortInteger}
- value={measures.ncloc}
- />
- </span>
-
- <span className="sw-body-sm">{translate('metric.ncloc.name')}</span>
- </div>
-
- <SeparatorCircleIcon className="sw-mx-1" />
-
- <span className="sw-body-sm" data-key={MetricKey.ncloc_language_distribution}>
- <ProjectCardLanguages distribution={measures.ncloc_language_distribution} />
- </span>
- </>
- )}
-
- {tags.length > 0 && (
- <>
- <SeparatorCircleIcon className="sw-mx-1" />
-
- <Tags
- className="sw-body-sm"
- emptyText={translate('issue.no_tag')}
- ariaTagsListLabel={translate('issue.tags')}
- tooltip={Tooltip}
- tags={tags}
- tagsToDisplay={2}
- />
- </>
- )}
- </LightLabel>
- </>
- );
- }
-
- function renderSecondLine(
- currentUser: Props['currentUser'],
- project: Props['project'],
- isNewCode: boolean,
- ) {
- const { analysisDate, key, leakPeriodDate, measures, qualifier, isScannable } = project;
-
- if (!isEmpty(analysisDate) && (!isNewCode || !isEmpty(leakPeriodDate))) {
- return (
- <ProjectCardMeasures
- measures={measures}
- componentQualifier={qualifier}
- isNewCode={isNewCode}
- />
- );
- }
-
- return (
- <div className="sw-flex sw-items-center">
- <Note className="sw-py-4">
- {isNewCode && analysisDate
- ? translate('projects.no_new_code_period', qualifier)
- : translate('projects.not_analyzed', qualifier)}
- </Note>
-
- {qualifier !== ComponentQualifier.Application &&
- isEmpty(analysisDate) &&
- isLoggedIn(currentUser) &&
- isScannable && (
- <Link className="sw-ml-2 sw-body-sm-highlight" to={getProjectUrl(key)}>
- {translate('projects.configure_analysis')}
- </Link>
- )}
- </div>
- );
- }
-
- export default function ProjectCard(props: Readonly<Props>) {
- const { currentUser, type, project } = props;
- const isNewCode = type === 'leak';
-
- return (
- <ProjectCardWrapper
- className={classNames(
- 'it_project_card sw-relative sw-box-border sw-rounded-1 sw-mb-page sw-h-full',
- )}
- data-key={project.key}
- >
- {renderFirstLine(project, props.handleFavorite, isNewCode)}
-
- <SubnavigationFlowSeparator className="sw-my-3" />
-
- {renderSecondLine(currentUser, project, isNewCode)}
- </ProjectCardWrapper>
- );
- }
-
- const ProjectCardWrapper = styled(Card)`
- background-color: ${themeColor('projectCardBackground')};
- border: ${themeBorder('default', 'projectCardBorder')};
- &.project-card-disabled *:not(g):not(path) {
- color: ${themeColor('projectCardDisabled')} !important;
- }
- `;
|