* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
-import { configure } from '@testing-library/dom';
import '@testing-library/jest-dom';
+import { configure } from '@testing-library/react';
configure({
- asyncUtilTimeout: 3000
+ asyncUtilTimeout: 3000,
});
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import React from 'react';
+
+(window as any).React = React;
+
const content = document.createElement('div');
content.id = 'content';
document.documentElement.appendChild(content);
`;
LinkBox.displayName = 'LinkBox';
+export const DiscreetLinkBox = styled(StyledBaseLink)`
+ text-decoration: none;
+
+ &:hover,
+ &:focus,
+ &:active {
+ background-color: none;
+ display: block;
+ }
+`;
+LinkBox.displayName = 'DiscreetLinkBox';
+
export const DiscreetLink = styled(HoverLink)`
--border: ${themeBorder('default', 'linkDiscreet')};
`;
const StyledTextError = styled(StyledText)`
color: ${themeColor('danger')};
`;
+
+export const LightLabel = styled.span`
+ color: ${themeColor('pageContentLight')};
+`;
+
+export const LightPrimary = styled.span`
+ color: ${themeContrast('primaryLight')};
+`;
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { HelperHintIcon } from 'design-system';
import * as React from 'react';
import HelpTooltip from '../../../components/controls/HelpTooltip';
import DateFromNow from '../../../components/intl/DateFromNow';
export function ApplicationLeakPeriodInfo({ leakPeriod }: ApplicationLeakPeriodInfoProps) {
return (
- <div className="note spacer-top display-inline-flex-center">
+ <>
<DateFromNow date={leakPeriod.date}>
{(fromNow) => translateWithParameters('overview.started_x', fromNow)}
</DateFromNow>
<HelpTooltip
- className="little-spacer-left"
+ className="sw-ml-1"
overlay={translateWithParameters(
'overview.max_new_code_period_from_x',
leakPeriod.projectName
)}
- />
- </div>
+ >
+ <HelperHintIcon />
+ </HelpTooltip>
+ </>
);
}
<NoCodeWarning branchLike={branch} component={component} measures={measures} />
) : (
<div className="sw-flex">
- <div className="width-30 sw-mr-12">
+ <div className="width-30 sw-mr-12 sw-pt-6">
<QualityGatePanel
component={component}
loading={loadingStatus}
</div>
<div className="sw-flex-1">
- <div className="sw-flex sw-flex-col">
+ <div className="sw-flex sw-flex-col sw-pt-6">
<MeasuresPanel
appLeak={appLeak}
branch={branch}
loading={loadingStatus}
measures={measures}
period={period}
+ qgStatuses={qgStatuses}
/>
<ActivityPanel
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { DrilldownLink } from 'design-system';
import * as React from 'react';
-import DrilldownLink from '../../../components/shared/DrilldownLink';
-import { getLocalizedMetricName, translateWithParameters } from '../../../helpers/l10n';
+import { translateWithParameters } from '../../../helpers/l10n';
import { findMeasure, formatMeasure, localizeMetric } from '../../../helpers/measures';
+import { getComponentDrilldownUrl } from '../../../helpers/urls';
import { BranchLike } from '../../../types/branch-like';
-import { MetricKey } from '../../../types/metrics';
+import { MetricKey, MetricType } from '../../../types/metrics';
import { Component, MeasureEnhanced } from '../../../types/types';
export interface DrilldownMeasureValueProps {
const { branchLike, component, measures, metric } = props;
const measure = findMeasure(measures, metric);
- let content;
if (!measure || measure.value === undefined) {
- content = <span className="overview-measures-value text-light">-</span>;
- } else {
- content = (
- <span>
- <DrilldownLink
- ariaLabel={translateWithParameters(
- 'overview.see_more_details_on_x_y',
- measure.value,
- localizeMetric(metric)
- )}
- branchLike={branchLike}
- className="overview-measures-value text-light"
- component={component.key}
- metric={metric}
- >
- {formatMeasure(measure.value, 'SHORT_INT')}
- </DrilldownLink>
- </span>
- );
+ return <span>–</span>;
}
+ const url = getComponentDrilldownUrl({
+ branchLike,
+ componentKey: component.key,
+ metric,
+ });
+
return (
- <div className="display-flex-column display-flex-center">
- {content}
- <span className="spacer-top">{getLocalizedMetricName({ key: metric })}</span>
- </div>
+ <span>
+ <DrilldownLink
+ aria-label={translateWithParameters(
+ 'overview.see_more_details_on_x_y',
+ measure.value,
+ localizeMetric(metric)
+ )}
+ to={url}
+ >
+ {formatMeasure(measure.value, MetricType.ShortInteger)}
+ </DrilldownLink>
+ </span>
);
}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import {
+ Card,
+ CoverageIndicator,
+ DeferredSpinner,
+ DuplicationsIndicator,
+ LightLabel,
+ PageTitle,
+ ToggleButton,
+} from 'design-system';
import * as React from 'react';
-import { rawSizes } from '../../../app/theme';
-import BoxedTabs, { getTabId, getTabPanelId } from '../../../components/controls/BoxedTabs';
import ComponentReportActions from '../../../components/controls/ComponentReportActions';
import { Location, withRouter } from '../../../components/hoc/withRouter';
-import DeferredSpinner from '../../../components/ui/DeferredSpinner';
-import { translate } from '../../../helpers/l10n';
+import { duplicationRatingConverter } from '../../../components/measure/utils';
+import { translate, translateWithParameters } from '../../../helpers/l10n';
import { findMeasure, isDiffMetric } from '../../../helpers/measures';
import { CodeScope } from '../../../helpers/urls';
import { ApplicationPeriod } from '../../../types/application';
import { ComponentQualifier } from '../../../types/component';
import { IssueType } from '../../../types/issues';
import { MetricKey } from '../../../types/metrics';
+import { QualityGateStatus } from '../../../types/quality-gates';
import { Component, MeasureEnhanced, Period } from '../../../types/types';
-import MeasurementLabel from '../components/MeasurementLabel';
import { MeasurementType, parseQuery } from '../utils';
-import { DrilldownMeasureValue } from './DrilldownMeasureValue';
import { LeakPeriodInfo } from './LeakPeriodInfo';
-import MeasuresPanelIssueMeasureRow from './MeasuresPanelIssueMeasureRow';
+import MeasuresPanelIssueMeasure from './MeasuresPanelIssueMeasure';
import MeasuresPanelNoNewCode from './MeasuresPanelNoNewCode';
+import MeasuresPanelPercentMeasure from './MeasuresPanelPercentMeasure';
export interface MeasuresPanelProps {
appLeak?: ApplicationPeriod;
measures?: MeasureEnhanced[];
period?: Period;
location: Location;
+ qgStatuses?: QualityGateStatus[];
}
export enum MeasuresPanelTabs {
}
export function MeasuresPanel(props: MeasuresPanelProps) {
- const { appLeak, branch, component, loading, measures = [], period, location } = props;
+ const {
+ appLeak,
+ branch,
+ component,
+ loading,
+ measures = [],
+ period,
+ qgStatuses = [],
+ location,
+ } = props;
const hasDiffMeasures = measures.some((m) => isDiffMetric(m.metric.key));
const isApp = component.qualifier === ComponentQualifier.Application;
const leakPeriod = isApp ? appLeak : period;
const query = parseQuery(location.query);
+ const { failingConditionsOnNewCode, failingConditionsOnOverallCode } =
+ countFailingConditions(qgStatuses);
+ const failingConditions = failingConditionsOnNewCode + failingConditionsOnOverallCode;
+
const [tab, selectTab] = React.useState(() => {
return query.codeScope === CodeScope.Overall
? MeasuresPanelTabs.Overall
const tabs = [
{
- key: MeasuresPanelTabs.New,
- label: (
- <div className="text-left overview-measures-tab">
- <span className="text-bold">{translate('overview.new_code')}</span>
- {leakPeriod && <LeakPeriodInfo leakPeriod={leakPeriod} />}
- </div>
- ),
+ value: MeasuresPanelTabs.New,
+ label: translate('overview.new_code'),
+ counter: failingConditionsOnNewCode,
},
{
- key: MeasuresPanelTabs.Overall,
- label: (
- <div className="text-left overview-measures-tab">
- <span className="text-bold" style={{ position: 'absolute', top: 2 * rawSizes.grid }}>
- {translate('overview.overall_code')}
- </span>
- </div>
- ),
+ value: MeasuresPanelTabs.Overall,
+ label: translate('overview.overall_code'),
+ counter: failingConditionsOnOverallCode,
},
];
return (
- <div className="overview-panel" data-test="overview__measures-panel">
- <div className="display-flex-space-between display-flex-start">
- <h2 className="overview-panel-title">{translate('overview.measures')}</h2>
+ <div data-test="overview__measures-panel">
+ <div className="sw-float-right -sw-mt-6">
<ComponentReportActions component={component} branch={branch} />
</div>
+ <h2 className="sw-flex sw-mb-4">
+ <PageTitle text={translate('overview.measures')} />
+ </h2>
{loading ? (
- <div className="overview-panel-content overview-panel-big-padded">
+ <div>
<DeferredSpinner loading={loading} />
</div>
) : (
<>
- <BoxedTabs onSelect={(key) => selectTab(key)} selected={tab} tabs={tabs} />
-
- <div
- className="overview-panel-content flex-1 bordered"
- role="tabpanel"
- id={getTabPanelId(tab)}
- aria-labelledby={getTabId(tab)}
- >
- {!hasDiffMeasures && isNewCodeTab ? (
- <MeasuresPanelNoNewCode branch={branch} component={component} period={period} />
- ) : (
- <>
- {[
- IssueType.Bug,
- IssueType.Vulnerability,
- IssueType.SecurityHotspot,
- IssueType.CodeSmell,
- ].map((type: IssueType) => (
- <MeasuresPanelIssueMeasureRow
+ <div className="sw-flex sw-items-center">
+ <ToggleButton onChange={(key) => selectTab(key)} options={tabs} value={tab} />
+ {failingConditions > 0 && (
+ <LightLabel className="sw-body-sm-highlight sw-ml-8">
+ {translateWithParameters('overview.X_conditions_failed', failingConditions)}
+ </LightLabel>
+ )}
+ </div>
+
+ {tab === MeasuresPanelTabs.New && leakPeriod ? (
+ <LightLabel className="sw-body-sm sw-flex sw-items-center sw-mt-4">
+ <span className="sw-mr-1">{translate('overview.new_code')}:</span>
+ <LeakPeriodInfo leakPeriod={leakPeriod} />
+ </LightLabel>
+ ) : (
+ <div className="sw-h-4 sw-pt-1 sw-mt-4" />
+ )}
+
+ {!hasDiffMeasures && isNewCodeTab ? (
+ <MeasuresPanelNoNewCode branch={branch} component={component} period={period} />
+ ) : (
+ <div className="sw-grid sw-grid-cols-2 sw-gap-4 sw-mt-4">
+ {[
+ IssueType.Bug,
+ IssueType.CodeSmell,
+ IssueType.Vulnerability,
+ IssueType.SecurityHotspot,
+ ].map((type: IssueType) => (
+ <Card key={type} className="sw-p-8">
+ <MeasuresPanelIssueMeasure
branchLike={branch}
component={component}
isNewCodeTab={isNewCodeTab}
- key={type}
measures={measures}
type={type}
/>
- ))}
-
- <div className="display-flex-row overview-measures-row">
- {(findMeasure(measures, MetricKey.coverage) ||
- findMeasure(measures, MetricKey.new_coverage)) && (
- <div
- className="overview-panel-huge-padded flex-1 bordered-right display-flex-center"
- data-test="overview__measures-coverage"
- >
- <MeasurementLabel
- branchLike={branch}
- centered={isNewCodeTab}
- component={component}
- measures={measures}
- type={MeasurementType.Coverage}
- useDiffMetric={isNewCodeTab}
- />
-
- {tab === MeasuresPanelTabs.Overall && (
- <div className="huge-spacer-left">
- <DrilldownMeasureValue
- branchLike={branch}
- component={component}
- measures={measures}
- metric={MetricKey.tests}
- />
- </div>
- )}
- </div>
- )}
- <div className="overview-panel-huge-padded flex-1 display-flex-center">
- <MeasurementLabel
- branchLike={branch}
- centered={isNewCodeTab}
- component={component}
- measures={measures}
- type={MeasurementType.Duplication}
- useDiffMetric={isNewCodeTab}
- />
-
- {tab === MeasuresPanelTabs.Overall && (
- <div className="huge-spacer-left">
- <DrilldownMeasureValue
- branchLike={branch}
- component={component}
- measures={measures}
- metric={MetricKey.duplicated_blocks}
- />
- </div>
- )}
- </div>
- </div>
- </>
- )}
- </div>
+ </Card>
+ ))}
+
+ {(findMeasure(measures, MetricKey.coverage) ||
+ findMeasure(measures, MetricKey.new_coverage)) && (
+ <Card className="sw-p-8" data-test="overview__measures-coverage">
+ <MeasuresPanelPercentMeasure
+ branchLike={branch}
+ component={component}
+ measures={measures}
+ ratingIcon={renderCoverageIcon}
+ secondaryMetricKey={MetricKey.tests}
+ type={MeasurementType.Coverage}
+ useDiffMetric={isNewCodeTab}
+ />
+ </Card>
+ )}
+
+ <Card className="sw-p-8">
+ <MeasuresPanelPercentMeasure
+ branchLike={branch}
+ component={component}
+ measures={measures}
+ ratingIcon={renderDuplicationIcon}
+ secondaryMetricKey={MetricKey.duplicated_blocks}
+ type={MeasurementType.Duplication}
+ useDiffMetric={isNewCodeTab}
+ />
+ </Card>
+ </div>
+ )}
</>
)}
</div>
}
export default withRouter(React.memo(MeasuresPanel));
+
+function renderCoverageIcon(value?: string) {
+ return <CoverageIndicator value={value} size="md" />;
+}
+
+function renderDuplicationIcon(value?: string) {
+ const rating = value !== undefined ? duplicationRatingConverter(Number(value)) : undefined;
+
+ return <DuplicationsIndicator rating={rating} size="md" />;
+}
+
+function countFailingConditions(qgStatuses: QualityGateStatus[]) {
+ let failingConditionsOnNewCode = 0;
+ let failingConditionsOnOverallCode = 0;
+
+ qgStatuses.forEach(({ failedConditions }) => {
+ failedConditions.forEach((condition) => {
+ if (isDiffMetric(condition.metric)) {
+ failingConditionsOnNewCode += 1;
+ } else {
+ failingConditionsOnOverallCode += 1;
+ }
+ });
+ });
+
+ return { failingConditionsOnNewCode, failingConditionsOnOverallCode };
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 * as React from 'react';
+
+interface Props {
+ category: React.ReactElement;
+ rating: React.ReactElement | null;
+}
+
+export default function MeasuresPanelCard(
+ props: React.PropsWithChildren<Props & React.HTMLAttributes<HTMLDivElement>>
+) {
+ const { category, children, rating, ...attributes } = props;
+
+ return (
+ <div className="sw-flex sw-justify-between sw-items-center" {...attributes}>
+ <div className="sw-flex sw-flex-col sw-justify-between">
+ <div className="sw-body-sm-highlight sw-flex sw-items-center">{category}</div>
+
+ <div className="sw-mt-3">{children}</div>
+ </div>
+
+ <div>{rating}</div>
+ </div>
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { LightPrimary, ThemeColors } from 'design-system';
+import * as React from 'react';
+import { translate } from '../../../helpers/l10n';
+import { BranchLike } from '../../../types/branch-like';
+import { ComponentQualifier } from '../../../types/component';
+import { IssueType } from '../../../types/issues';
+import { Component, MeasureEnhanced } from '../../../types/types';
+import IssueLabel from '../components/IssueLabel';
+import IssueRating from '../components/IssueRating';
+import { getIssueIconClass, getIssueRatingName } from '../utils';
+import MeasuresPanelCard from './MeasuresPanelCard';
+
+interface Props {
+ branchLike?: BranchLike;
+ component: Component;
+ isNewCodeTab: boolean;
+ measures: MeasureEnhanced[];
+ type: IssueType;
+}
+
+export default function MeasuresPanelIssueMeasure(props: Props) {
+ const { branchLike, component, isNewCodeTab, measures, type } = props;
+
+ const isApp = component.qualifier === ComponentQualifier.Application;
+
+ const IconClass = getIssueIconClass(type) as (args: {
+ className?: string;
+ fill?: ThemeColors;
+ }) => JSX.Element;
+
+ return (
+ <MeasuresPanelCard
+ data-test={`overview__measures-${type.toString().toLowerCase()}`}
+ category={
+ <div className="sw-flex sw-items-center">
+ <IconClass className="sw-mr-1" fill="discreetInteractiveIcon" />
+ <LightPrimary>{getIssueRatingName(type)}</LightPrimary>
+ </div>
+ }
+ rating={
+ !isApp || !isNewCodeTab ? (
+ <IssueRating
+ branchLike={branchLike}
+ component={component}
+ measures={measures}
+ type={type}
+ useDiffMetric={isNewCodeTab}
+ />
+ ) : null
+ }
+ >
+ <IssueLabel
+ branchLike={branchLike}
+ component={component}
+ helpTooltip={
+ type === IssueType.SecurityHotspot
+ ? translate('metric.security_hotspots.full_description')
+ : undefined
+ }
+ measures={measures}
+ type={type}
+ useDiffMetric={isNewCodeTab}
+ />
+ </MeasuresPanelCard>
+ );
+}
+++ /dev/null
-/*
- * SonarQube
- * Copyright (C) 2009-2023 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 * as React from 'react';
-import { translate } from '../../../helpers/l10n';
-import { BranchLike } from '../../../types/branch-like';
-import { ComponentQualifier } from '../../../types/component';
-import { IssueType } from '../../../types/issues';
-import { Component, MeasureEnhanced } from '../../../types/types';
-import IssueLabel from '../components/IssueLabel';
-import IssueRating from '../components/IssueRating';
-import DebtValue from './DebtValue';
-import SecurityHotspotsReviewed from './SecurityHotspotsReviewed';
-
-export interface MeasuresPanelIssueMeasureRowProps {
- branchLike?: BranchLike;
- component: Component;
- isNewCodeTab: boolean;
- measures: MeasureEnhanced[];
- type: IssueType;
-}
-
-export default function MeasuresPanelIssueMeasureRow(props: MeasuresPanelIssueMeasureRowProps) {
- const { branchLike, component, isNewCodeTab, measures, type } = props;
-
- const isApp = component.qualifier === ComponentQualifier.Application;
-
- return (
- <div
- className="display-flex-row overview-measures-row"
- data-test={`overview__measures-${type.toString().toLowerCase()}`}
- >
- {type === IssueType.CodeSmell ? (
- <>
- <div className="overview-panel-big-padded flex-1 small display-flex-center big-spacer-left">
- <DebtValue
- branchLike={branchLike}
- component={component}
- measures={measures}
- useDiffMetric={isNewCodeTab}
- />
- </div>
- <div className="flex-1 small display-flex-center">
- <IssueLabel
- branchLike={branchLike}
- component={component}
- measures={measures}
- type={type}
- useDiffMetric={isNewCodeTab}
- />
- </div>
- </>
- ) : (
- <div className="overview-panel-big-padded flex-1 small display-flex-center big-spacer-left">
- <IssueLabel
- branchLike={branchLike}
- component={component}
- helpTooltip={
- type === IssueType.SecurityHotspot
- ? translate('metric.security_hotspots.full_description')
- : undefined
- }
- measures={measures}
- type={type}
- useDiffMetric={isNewCodeTab}
- />
- </div>
- )}
- {type === IssueType.SecurityHotspot && (
- <div className="flex-1 small display-flex-center">
- <SecurityHotspotsReviewed measures={measures} useDiffMetric={isNewCodeTab} />
- </div>
- )}
- {(!isApp || !isNewCodeTab) && (
- <div className="overview-panel-big-padded overview-measures-aside display-flex-center">
- <IssueRating
- branchLike={branchLike}
- component={component}
- measures={measures}
- type={type}
- useDiffMetric={isNewCodeTab}
- />
- </div>
- )}
- </div>
- );
-}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { DrilldownLink, GreySeparator, LightLabel, LightPrimary } from 'design-system';
+import * as React from 'react';
+import { getLeakValue } from '../../../components/measure/utils';
+import { isPullRequest } from '../../../helpers/branch-like';
+import { getLocalizedMetricName, translate, translateWithParameters } from '../../../helpers/l10n';
+import { findMeasure, formatMeasure, localizeMetric } from '../../../helpers/measures';
+import { getComponentDrilldownUrl } from '../../../helpers/urls';
+import { BranchLike } from '../../../types/branch-like';
+import { MetricKey, MetricType } from '../../../types/metrics';
+import { Component, MeasureEnhanced } from '../../../types/types';
+import AfterMergeEstimate from '../pullRequests/AfterMergeEstimate';
+import { MeasurementType, getMeasurementMetricKey } from '../utils';
+import DrilldownMeasureValue from './DrilldownMeasureValue';
+import MeasuresPanelCard from './MeasuresPanelCard';
+import MeasuresPanelPercentMeasureLabel from './MeasuresPanelPercentMeasureLabel';
+
+interface Props {
+ branchLike?: BranchLike;
+ component: Component;
+ useDiffMetric: boolean;
+ measures: MeasureEnhanced[];
+ ratingIcon: (value: string | undefined) => React.ReactElement;
+ secondaryMetricKey?: MetricKey;
+ type: MeasurementType;
+}
+
+export default function MeasuresPanelPercentMeasure(props: Props) {
+ const {
+ branchLike,
+ component,
+ measures,
+ ratingIcon,
+ secondaryMetricKey,
+ type,
+ useDiffMetric = false,
+ } = props;
+ const metricKey = getMeasurementMetricKey(type, useDiffMetric);
+ const measure = findMeasure(measures, metricKey);
+
+ let value;
+ if (measure) {
+ value = useDiffMetric ? getLeakValue(measure) : measure.value;
+ }
+
+ const url = getComponentDrilldownUrl({
+ componentKey: component.key,
+ metric: metricKey,
+ branchLike,
+ listView: true,
+ });
+
+ const formattedValue = formatMeasure(value, MetricType.Percent, {
+ decimals: 2,
+ omitExtraDecimalZeros: true,
+ });
+
+ return (
+ <MeasuresPanelCard
+ data-test={`overview__measures-${type.toString().toLowerCase()}`}
+ category={<LightPrimary>{translate('overview.measurement_type', type)}</LightPrimary>}
+ rating={ratingIcon(value)}
+ >
+ <>
+ <div className="sw-body-md sw-flex sw-items-center sw-mb-3">
+ {value === undefined ? (
+ <LightLabel aria-label={translate('no_data')}> — </LightLabel>
+ ) : (
+ <DrilldownLink
+ aria-label={translateWithParameters(
+ 'overview.see_more_details_on_x_of_y',
+ value,
+ localizeMetric(metricKey)
+ )}
+ to={url}
+ >
+ {formattedValue}
+ </DrilldownLink>
+ )}
+
+ <LightLabel className="sw-ml-2">
+ {translate('overview.measurement_type', type)}
+ </LightLabel>
+ </div>
+ <MeasuresPanelPercentMeasureLabel
+ component={component}
+ measures={measures}
+ type={type}
+ useDiffMetric={useDiffMetric}
+ branchLike={branchLike}
+ />
+
+ {!useDiffMetric && secondaryMetricKey && (
+ <>
+ <GreySeparator className="sw-mt-4" />
+ <div className="sw-body-md sw-flex sw-items-center sw-mt-4">
+ <DrilldownMeasureValue
+ branchLike={branchLike}
+ component={component}
+ measures={measures}
+ metric={secondaryMetricKey}
+ />
+ <LightLabel className="sw-ml-2">
+ {getLocalizedMetricName({ key: secondaryMetricKey })}
+ </LightLabel>
+ </div>
+ </>
+ )}
+
+ {isPullRequest(branchLike) && (
+ <>
+ <GreySeparator className="sw-mt-4" />
+ <div className="sw-body-md sw-flex sw-items-center sw-mt-4">
+ <AfterMergeEstimate measures={measures} type={type} />
+ <LightLabel className="sw-ml-2">
+ {translate('component_measures.facet_category.overall_category.estimated')}
+ </LightLabel>
+ </div>
+ </>
+ )}
+ </>
+ </MeasuresPanelCard>
+ );
+}
--- /dev/null
+/*
+ * SonarQube
+ * Copyright (C) 2009-2023 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 { DrilldownLink, LightLabel } from 'design-system';
+import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { getLeakValue } from '../../../components/measure/utils';
+import { translate } from '../../../helpers/l10n';
+import { findMeasure, formatMeasure } from '../../../helpers/measures';
+import { getComponentDrilldownUrl } from '../../../helpers/urls';
+import { BranchLike } from '../../../types/branch-like';
+import { MetricType } from '../../../types/metrics';
+import { Component, MeasureEnhanced } from '../../../types/types';
+import { MeasurementType, getMeasurementLabelKeys, getMeasurementLinesMetricKey } from '../utils';
+
+interface Props {
+ branchLike?: BranchLike;
+ component: Component;
+ useDiffMetric: boolean;
+ measures: MeasureEnhanced[];
+ type: MeasurementType;
+}
+
+export default function MeasuresPanelPercentMeasureLabel(props: Props) {
+ const { branchLike, component, measures, type, useDiffMetric = false } = props;
+ const { expandedLabelKey, labelKey } = getMeasurementLabelKeys(type, useDiffMetric);
+ const linesMetric = getMeasurementLinesMetricKey(type, useDiffMetric);
+ const measure = findMeasure(measures, linesMetric);
+
+ if (!measure) {
+ return <LightLabel>{translate(labelKey)}</LightLabel>;
+ }
+
+ const value = useDiffMetric ? getLeakValue(measure) : measure.value;
+
+ const url = getComponentDrilldownUrl({
+ componentKey: component.key,
+ metric: linesMetric,
+ branchLike,
+ listView: true,
+ });
+
+ return (
+ <LightLabel>
+ <FormattedMessage
+ defaultMessage={translate(expandedLabelKey)}
+ id={expandedLabelKey}
+ values={{
+ count: (
+ <DrilldownLink className="sw-body-md-highlight" to={url}>
+ {formatMeasure(value, MetricType.ShortInteger)}
+ </DrilldownLink>
+ ),
+ }}
+ />
+ </LightLabel>
+ );
+}
leakPeriod.mode === NewCodePeriodSettingType.NUMBER_OF_DAYS ||
leakPeriod.mode === NewCodePeriodSettingType.REFERENCE_BRANCH
) {
- return <div className="note spacer-top">{leakPeriodLabel} </div>;
+ return <div>{leakPeriodLabel} </div>;
}
const leakPeriodDate = getPeriodDate(leakPeriod);
return (
<>
- <div className="note spacer-top text-ellipsis" title={leakPeriodLabel}>
+ <div className="sw-mr-2 sw-text-ellipsis" title={leakPeriodLabel}>
{leakPeriodLabel}
</div>
+
<DateFromNow date={leakPeriodDate}>
- {(fromNow) => (
- <div className="note little-spacer-top">
- {translateWithParameters(
- leakPeriod.mode === 'previous_analysis'
- ? 'overview.previous_analysis_x'
- : 'overview.started_x',
- fromNow
- )}
- </div>
- )}
+ {(fromNow) =>
+ translateWithParameters(
+ leakPeriod.mode === 'previous_analysis'
+ ? 'overview.previous_analysis_x'
+ : 'overview.started_x',
+ fromNow
+ )
+ }
</DateFromNow>
</>
);
renderBranchOverview();
expect(await screen.findByText('metric.level.ERROR')).toBeInTheDocument();
- expect(screen.getByText('overview.X_conditions_failed.2')).toBeInTheDocument();
+ expect(screen.getAllByText('overview.X_conditions_failed.2')).toHaveLength(2);
});
it('should correctly show a project as empty', async () => {
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { DrilldownLink, HelperHintIcon, LightLabel } from 'design-system';
import * as React from 'react';
-import Link from '../../../components/common/Link';
import HelpTooltip from '../../../components/controls/HelpTooltip';
import { getLeakValue } from '../../../components/measure/utils';
import { getBranchLikeQuery } from '../../../helpers/branch-like';
import { getComponentIssuesUrl, getComponentSecurityHotspotsUrl } from '../../../helpers/urls';
import { BranchLike } from '../../../types/branch-like';
import { IssueType } from '../../../types/issues';
+import { MetricType } from '../../../types/metrics';
import { Component, MeasureEnhanced } from '../../../types/types';
-import { getIssueIconClass, getIssueMetricKey } from '../utils';
+import { getIssueMetricKey } from '../utils';
export interface IssueLabelProps {
branchLike?: BranchLike;
const { branchLike, component, helpTooltip, measures, type, useDiffMetric = false } = props;
const metricKey = getIssueMetricKey(type, useDiffMetric);
const measure = findMeasure(measures, metricKey);
- const iconClass = getIssueIconClass(type);
let value;
if (measure) {
: getComponentIssuesUrl(component.key, params);
return (
- <>
+ <div className="sw-body-md sw-flex sw-items-center">
{value === undefined ? (
- <span aria-label={translate('no_data')} className="overview-measures-empty-value" />
+ <LightLabel aria-label={translate('no_data')}> — </LightLabel>
) : (
- <Link
+ <DrilldownLink
aria-label={translateWithParameters(
'overview.see_list_of_x_y_issues',
value,
localizeMetric(metricKey)
)}
- className="overview-measures-value text-light"
+ className="it__overview-measures-value"
to={url}
>
- {formatMeasure(value, 'SHORT_INT')}
- </Link>
+ {formatMeasure(value, MetricType.ShortInteger)}
+ </DrilldownLink>
)}
- {React.createElement(iconClass, { className: 'big-spacer-left little-spacer-right' })}
- {localizeMetric(metricKey)}
- {helpTooltip && <HelpTooltip className="little-spacer-left" overlay={helpTooltip} />}
- </>
+ <LightLabel className="sw-mx-2">{localizeMetric(metricKey)}</LightLabel>
+ {helpTooltip && (
+ <HelpTooltip overlay={helpTooltip}>
+ <HelperHintIcon aria-label={helpTooltip} />
+ </HelpTooltip>
+ )}
+ </div>
);
}
*/
/* eslint-disable react/no-unused-prop-types */
+import { DiscreetLinkBox, MetricsRatingBadge } from 'design-system';
import * as React from 'react';
import Tooltip from '../../../components/controls/Tooltip';
import RatingTooltipContent from '../../../components/measure/RatingTooltipContent';
import { getLeakValue } from '../../../components/measure/utils';
-import DrilldownLink from '../../../components/shared/DrilldownLink';
-import Rating from '../../../components/ui/Rating';
-import { findMeasure } from '../../../helpers/measures';
+import { translateWithParameters } from '../../../helpers/l10n';
+import { findMeasure, formatRating } from '../../../helpers/measures';
+import { getComponentDrilldownUrl } from '../../../helpers/urls';
import { BranchLike } from '../../../types/branch-like';
import { IssueType } from '../../../types/issues';
import { Component, MeasureEnhanced } from '../../../types/types';
-import { getIssueRatingMetricKey, getIssueRatingName } from '../utils';
+import { getIssueRatingMetricKey } from '../utils';
export interface IssueRatingProps {
branchLike?: BranchLike;
useDiffMetric?: boolean;
}
-function renderRatingLink(props: IssueRatingProps) {
+export function IssueRating(props: IssueRatingProps) {
const { branchLike, component, useDiffMetric = false, measures, type } = props;
- const rating = getIssueRatingMetricKey(type, useDiffMetric);
- const measure = findMeasure(measures, rating);
+ const ratingKey = getIssueRatingMetricKey(type, useDiffMetric);
+ const measure = findMeasure(measures, ratingKey);
+ const rawValue = measure && (useDiffMetric ? getLeakValue(measure) : measure.value);
+ const value = formatRating(rawValue);
- if (!rating || !measure) {
- return (
- <div className="padded">
- <Rating value={undefined} />
- </div>
- );
+ if (!ratingKey || !measure) {
+ return <NoRating />;
}
- const value = measure && (useDiffMetric ? getLeakValue(measure) : measure.value);
-
return (
- <Tooltip overlay={value && <RatingTooltipContent metricKey={rating} value={value} />}>
+ <Tooltip overlay={rawValue && <RatingTooltipContent metricKey={ratingKey} value={rawValue} />}>
<span>
- <DrilldownLink
- branchLike={branchLike}
- className="link-no-underline link-rating"
- component={component.key}
- metric={rating}
- >
- <Rating value={value} />
- </DrilldownLink>
+ {value ? (
+ <DiscreetLinkBox
+ to={getComponentDrilldownUrl({
+ branchLike,
+ componentKey: component.key,
+ metric: ratingKey,
+ listView: true,
+ })}
+ >
+ <MetricsRatingBadge
+ label={translateWithParameters('metric.has_rating_X', value)}
+ rating={value}
+ size="md"
+ />
+ </DiscreetLinkBox>
+ ) : (
+ <NoRating />
+ )}
</span>
</Tooltip>
);
}
-export function IssueRating(props: IssueRatingProps) {
- const { type } = props;
+export default IssueRating;
- return (
- <>
- <span className="flex-1 big-spacer-right text-right">{getIssueRatingName(type)}</span>
- {renderRatingLink(props)}
- </>
- );
+function NoRating() {
+ return <div className="sw-w-8 sw-h-8 sw-flex sw-justify-center sw-items-center">–</div>;
}
-
-export default React.memo(IssueRating);
export function QualityGateStatusTitle() {
return (
<div className="sw-flex sw-items-center sw-mb-4">
- <PageTitle text={translate('overview.quality_gate.status')} />
- <HelpTooltip
- className="sw-ml-2"
- overlay={<div className="sw-my-4">{translate('overview.quality_gate.help')}</div>}
- >
- <HelperHintIcon aria-label="help-tooltip" />
- </HelpTooltip>
+ <h2 className="sw-flex sw-items-center">
+ <PageTitle text={translate('overview.quality_gate.status')} />
+ <HelpTooltip
+ className="sw-ml-2"
+ overlay={<div className="sw-my-4">{translate('overview.quality_gate.help')}</div>}
+ >
+ <HelperHintIcon aria-label="help-tooltip" />
+ </HelpTooltip>
+ </h2>
</div>
);
}
import { mockPullRequest } from '../../../../helpers/mocks/branch-like';
import { mockComponent } from '../../../../helpers/mocks/component';
import { mockMeasureEnhanced, mockMetric } from '../../../../helpers/testMocks';
-import { findTooltipWithContent, renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { renderComponent } from '../../../../helpers/testReactTestingUtils';
import { IssueType } from '../../../../types/issues';
import { MetricKey } from '../../../../types/metrics';
import { IssueLabel, IssueLabelProps } from '../IssueLabel';
})
).toBeInTheDocument();
- expect(findTooltipWithContent('tooltip text')).toBeInTheDocument();
+ expect(screen.getByText('tooltip text')).toBeInTheDocument();
});
function renderIssueLabel(props: Partial<IssueLabelProps> = {}) {
import { IssueRating, IssueRatingProps } from '../IssueRating';
it('should render correctly for vulnerabilities', async () => {
- renderIssueRating({ type: IssueType.Vulnerability });
- expect(await screen.findByText('metric_domain.Security')).toBeInTheDocument();
-
renderIssueRating({ type: IssueType.Vulnerability, useDiffMetric: true });
- const labels = await screen.findAllByText('metric_domain.Security');
- expect(labels).toHaveLength(2);
- const tooltips = await screen.findAllByText('metric.security_rating.tooltip.A');
- expect(tooltips).toHaveLength(2);
+ expect(await screen.findByLabelText('metric.has_rating_X.A')).toBeInTheDocument();
+ expect(await screen.findByText('metric.security_rating.tooltip.A')).toBeInTheDocument();
});
it('should render correctly if no values are present', async () => {
renderIssueRating({
measures: [mockMeasureEnhanced({ metric: mockMetric({ key: 'NONE' }) })],
});
- expect(await screen.findByText('metric_domain.Reliability')).toBeInTheDocument();
+ expect(await screen.findByText('–')).toBeInTheDocument();
});
function renderIssueRating(props: Partial<IssueRatingProps> = {}) {
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import classNames from 'classnames';
+import { LightPrimary } from 'design-system';
import * as React from 'react';
-import { translate } from '../../../helpers/l10n';
import { findMeasure, formatMeasure } from '../../../helpers/measures';
+import { MetricType } from '../../../types/metrics';
import { MeasureEnhanced } from '../../../types/types';
-import { getMeasurementAfterMergeMetricKey, MeasurementType } from '../utils';
+import { MeasurementType, getMeasurementAfterMergeMetricKey } from '../utils';
export interface AfterMergeEstimateProps {
className?: string;
}
return (
- <div className={classNames(className, 'display-flex-center')}>
- <span className="huge">{formatMeasure(measure.value, 'PERCENT')}</span>
- <span className="label flex-1 spacer-left text-right">
- {translate('component_measures.facet_category.overall_category.estimated')}
- </span>
+ <div className={classNames(className, 'sw-flex sw-items-center')}>
+ <LightPrimary className="sw-heading-lg">
+ {formatMeasure(measure.value, MetricType.Percent)}
+ </LightPrimary>
</div>
);
}
import {
BasicSeparator,
Card,
+ CoverageIndicator,
DeferredSpinner,
+ DuplicationsIndicator,
HelperHintIcon,
LargeCenteredLayout,
Link,
+ PageTitle,
TextMuted,
} from 'design-system';
import { differenceBy, uniq } from 'lodash';
import withBranchStatus from '../../../app/components/branch-status/withBranchStatus';
import withBranchStatusActions from '../../../app/components/branch-status/withBranchStatusActions';
import HelpTooltip from '../../../components/controls/HelpTooltip';
+import { duplicationRatingConverter } from '../../../components/measure/utils';
import { getBranchLikeQuery } from '../../../helpers/branch-like';
import { translate } from '../../../helpers/l10n';
import { enhanceConditionWithMeasure, enhanceMeasuresWithMetrics } from '../../../helpers/measures';
import { BranchStatusData, PullRequest } from '../../../types/branch-like';
import { IssueType } from '../../../types/issues';
import { Component, MeasureEnhanced } from '../../../types/types';
+import MeasuresPanelIssueMeasure from '../branches/MeasuresPanelIssueMeasure';
+import MeasuresPanelPercentMeasure from '../branches/MeasuresPanelPercentMeasure';
import IgnoredConditionWarning from '../components/IgnoredConditionWarning';
-import IssueLabel from '../components/IssueLabel';
-import IssueRating from '../components/IssueRating';
-import MeasurementLabel from '../components/MeasurementLabel';
import QualityGateConditions from '../components/QualityGateConditions';
import QualityGateStatusHeader from '../components/QualityGateStatusHeader';
import QualityGateStatusPassedView from '../components/QualityGateStatusPassedView';
import SonarLintPromotion from '../components/SonarLintPromotion';
import '../styles.css';
import { MeasurementType, PR_METRICS } from '../utils';
-import AfterMergeEstimate from './AfterMergeEstimate';
interface Props extends BranchStatusData, Pick<BranchStatusContextInterface, 'fetchBranchStatus'> {
branchLike: PullRequest;
<SonarLintPromotion qgConditions={conditions} />
</div>
- <div className="flex-1">
- <h2 className="overview-panel-title spacer-bottom small">
- {translate('overview.measures')}
+ <div className="sw-flex-1">
+ <h2 className="sw-body-md-highlight">
+ <PageTitle text={translate('overview.measures')} />
</h2>
- <div className="overview-panel-content">
+ <div className="sw-grid sw-grid-cols-2 sw-gap-4 sw-mt-4">
{[
IssueType.Bug,
IssueType.Vulnerability,
IssueType.SecurityHotspot,
IssueType.CodeSmell,
].map((type: IssueType) => (
- <div className="overview-measures-row display-flex-row" key={type}>
- <div className="overview-panel-big-padded flex-1 small display-flex-center">
- <IssueLabel
- branchLike={branchLike}
- component={component}
- measures={measures}
- type={type}
- useDiffMetric={true}
- />
- </div>
- <div className="overview-panel-big-padded overview-measures-aside display-flex-center">
- <IssueRating
- branchLike={branchLike}
- component={component}
- measures={measures}
- type={type}
- useDiffMetric={true}
- />
- </div>
- </div>
+ <Card key={type} className="sw-p-8">
+ <MeasuresPanelIssueMeasure
+ branchLike={branchLike}
+ component={component}
+ isNewCodeTab={true}
+ measures={measures}
+ type={type}
+ />
+ </Card>
))}
{[MeasurementType.Coverage, MeasurementType.Duplication].map(
(type: MeasurementType) => (
- <div className="overview-measures-row display-flex-row" key={type}>
- <div className="overview-panel-big-padded flex-1 small display-flex-center">
- <MeasurementLabel
- branchLike={branchLike}
- component={component}
- measures={measures}
- type={type}
- useDiffMetric={true}
- />
- </div>
-
- <AfterMergeEstimate
- className="overview-panel-big-padded overview-measures-aside text-right overview-measures-emphasis"
+ <Card key={type} className="sw-p-8">
+ <MeasuresPanelPercentMeasure
+ branchLike={branchLike}
+ component={component}
measures={measures}
+ ratingIcon={renderMeasureIcon(type)}
type={type}
+ useDiffMetric={true}
/>
- </div>
+ </Card>
)
)}
</div>
}
export default withBranchStatus(withBranchStatusActions(PullRequestOverview));
+
+function renderMeasureIcon(type: MeasurementType) {
+ if (type === MeasurementType.Coverage) {
+ return function CoverageIndicatorRenderer(value?: string) {
+ return <CoverageIndicator value={value} size="md" />;
+ };
+ }
+
+ return function renderDuplicationIcon(value?: string) {
+ const rating = duplicationRatingConverter(Number(value));
+
+ return <DuplicationsIndicator rating={rating} size="md" />;
+ };
+}
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
+import { BugIcon, CodeSmellIcon, SecurityHotspotIcon, VulnerabilityIcon } from 'design-system';
import { flatten, sortBy } from 'lodash';
-import BugIcon from '../components/icons/BugIcon';
-import CodeSmellIcon from '../components/icons/CodeSmellIcon';
-import SecurityHotspotIcon from '../components/icons/SecurityHotspotIcon';
-import VulnerabilityIcon from '../components/icons/VulnerabilityIcon';
import { IssueType, RawIssue } from '../types/issues';
import { MetricKey } from '../types/metrics';
import { Dict, Flow, FlowLocation, Issue, TextRange } from '../types/types';
(value: string | number, options?: any): string;
}
-/** Format a measure value for a given type */
+/**
+ * Format a measure value for a given type
+ * ! For Ratings, use formatRating instead
+ */
export function formatMeasure(
value: string | number | undefined,
type: string,
return useFormatter(value, formatter, options);
}
+type RatingValue = 'A' | 'B' | 'C' | 'D' | 'E';
+const RATING_VALUES: RatingValue[] = ['A', 'B', 'C', 'D', 'E'];
+export function formatRating(value: string | number | undefined): RatingValue | undefined {
+ if (!value) {
+ return undefined;
+ }
+
+ if (typeof value === 'string') {
+ value = parseInt(value, 10);
+ }
+
+ // rating is 1-5, adjust for 0-based indexing
+ return RATING_VALUES[value - 1];
+}
+
/** Return a localized metric name */
export function localizeMetric(metricKey: string): string {
return translate('metric', metricKey, 'name');
overview.gate.view.warnings=The view has warnings on the following quality gate conditions: {0}.
overview.gate.view.errors=The view failed the quality gate on the following conditions: {0}.
-overview.domain.duplications=Duplications
-overview.domain.size=Size
+overview.measurement_type.DUPLICATION=Duplications
+overview.measurement_type.COVERAGE=Coverage
overview.complexity_tooltip.function={0} functions have complexity around {1}
overview.complexity_tooltip.file={0} files have complexity around {1}