]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-20679 Show measures variations for each analysis in project overview
authorAmbroise C <ambroise.christea@sonarsource.com>
Fri, 13 Oct 2023 14:58:17 +0000 (16:58 +0200)
committersonartech <sonartech@sonarsource.com>
Tue, 17 Oct 2023 20:02:44 +0000 (20:02 +0000)
12 files changed:
server/sonar-web/design-system/src/components/icons/TrendDownIcon.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/icons/TrendUpIcon.tsx [new file with mode: 0644]
server/sonar-web/design-system/src/components/icons/index.ts
server/sonar-web/src/main/js/api/time-machine.ts
server/sonar-web/src/main/js/apps/overview/branches/ActivityPanel.tsx
server/sonar-web/src/main/js/apps/overview/branches/Analysis.tsx
server/sonar-web/src/main/js/apps/overview/branches/AnalysisVariations.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/branches/__tests__/ActivityPanel-it.tsx
server/sonar-web/src/main/js/apps/overview/utils.ts
server/sonar-web/src/main/js/components/activity-graph/__tests__/GraphsTooltips-it.tsx
server/sonar-web/src/main/js/types/project-activity.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

diff --git a/server/sonar-web/design-system/src/components/icons/TrendDownIcon.tsx b/server/sonar-web/design-system/src/components/icons/TrendDownIcon.tsx
new file mode 100644 (file)
index 0000000..764a072
--- /dev/null
@@ -0,0 +1,24 @@
+/*
+ * 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 { ArrowDownRightIcon as OcticonArrowDownRightIcon } from '@primer/octicons-react';
+import { OcticonHoc } from './Icon';
+
+export const TrendDownIcon = OcticonHoc(OcticonArrowDownRightIcon, 'TrendDownIcon');
diff --git a/server/sonar-web/design-system/src/components/icons/TrendUpIcon.tsx b/server/sonar-web/design-system/src/components/icons/TrendUpIcon.tsx
new file mode 100644 (file)
index 0000000..cbf92fe
--- /dev/null
@@ -0,0 +1,24 @@
+/*
+ * 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 { ArrowUpRightIcon as OcticonArrowUpRightIcon } from '@primer/octicons-react';
+import { OcticonHoc } from './Icon';
+
+export const TrendUpIcon = OcticonHoc(OcticonArrowUpRightIcon, 'TrendUpIcon');
index 8671cd76022d0b244a67ae3cffae6e6541d55441..a6264cb3a1fa7afac8f33a0ff43ca29d9711648f 100644 (file)
@@ -84,6 +84,8 @@ export { StatusReopenedIcon } from './StatusReopenedIcon';
 export { StatusResolvedIcon } from './StatusResolvedIcon';
 export { TestFileIcon } from './TestFileIcon';
 export { TrashIcon } from './TrashIcon';
+export { TrendDownIcon } from './TrendDownIcon';
+export { TrendUpIcon } from './TrendUpIcon';
 export { TriangleDownIcon } from './TriangleDownIcon';
 export { TriangleLeftIcon } from './TriangleLeftIcon';
 export { TriangleRightIcon } from './TriangleRightIcon';
index edd38f5dac8f66306e9ba3b6e982edee9ad3f723..616493ff70ff28a536dbf89a1ef32f35e81d00e3 100644 (file)
 import { throwGlobalError } from '../helpers/error';
 import { getJSON } from '../helpers/request';
 import { BranchParameters } from '../types/branch-like';
+import { MetricKey } from '../types/metrics';
 import { Paging } from '../types/types';
 
 export interface TimeMachineResponse {
   measures: {
-    metric: string;
+    metric: MetricKey;
     history: Array<{ date: string; value?: string }>;
   }[];
   paging: Paging;
index c3a2d77011aa86517b8cec9a61908846fa34231b..a5f6c732e78419ad38f5df9ae70352fb1073fdc9 100644 (file)
@@ -38,6 +38,7 @@ import {
   MeasureHistory,
 } from '../../../types/project-activity';
 import { Component, Metric } from '../../../types/types';
+import { getAnalysisVariations } from '../utils';
 import Analysis from './Analysis';
 
 export interface ActivityPanelProps {
@@ -88,7 +89,16 @@ export function ActivityPanel(props: ActivityPanelProps) {
       startDate.getTime() > leakPeriodDate.getTime() ? startDate : leakPeriodDate;
   }
 
-  const filteredAnalyses = analyses.filter((a) => a.events.length > 0).slice(0, MAX_ANALYSES_NB);
+  const displayedAnalyses = analyses.slice(0, MAX_ANALYSES_NB);
+
+  const analysisVariations = React.useMemo(
+    () =>
+      getAnalysisVariations(
+        measuresHistory,
+        Math.min(analyses.length, MAX_ANALYSES_NB + 1),
+      ).reverse(),
+    [measuresHistory, analyses.length],
+  );
 
   return (
     <div className="sw-mt-8">
@@ -116,13 +126,18 @@ export function ActivityPanel(props: ActivityPanelProps) {
       </Card>
       <Card className="sw-mt-4" data-test="overview__activity-analyses">
         <Spinner loading={loading}>
-          {filteredAnalyses.length === 0 ? (
+          {displayedAnalyses.length === 0 ? (
             <p>{translate('no_results')}</p>
           ) : (
-            filteredAnalyses.map((analysis, index) => (
+            displayedAnalyses.map((analysis, index) => (
               <div key={analysis.key}>
-                <Analysis analysis={analysis} qualifier={component.qualifier} />
-                {index !== filteredAnalyses.length - 1 && <BasicSeparator className="sw-my-3" />}
+                <Analysis
+                  analysis={analysis}
+                  isFirstAnalysis={index === analyses.length - 1}
+                  qualifier={component.qualifier}
+                  variations={analysisVariations[index]}
+                />
+                {index !== displayedAnalyses.length - 1 && <BasicSeparator className="sw-my-3" />}
               </div>
             ))
           )}
index 7c729d4cb512eb1e41e9cd7d7b281707a4f45f24..88507be61db33ea6e9cfe10f1c8c4c97d1ff7754 100644 (file)
@@ -23,17 +23,23 @@ import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
 import { translate } from '../../../helpers/l10n';
 import { ComponentQualifier } from '../../../types/component';
 import {
+  AnalysisMeasuresVariations,
   ProjectAnalysisEventCategory,
   Analysis as TypeAnalysis,
 } from '../../../types/project-activity';
+import { AnalysisVariations } from './AnalysisVariations';
 import Event from './Event';
 
 export interface AnalysisProps {
   analysis: TypeAnalysis;
+  isFirstAnalysis?: boolean;
   qualifier: string;
+  variations?: AnalysisMeasuresVariations;
 }
 
-export function Analysis({ analysis, ...props }: AnalysisProps) {
+export function Analysis(props: Readonly<AnalysisProps>) {
+  const { analysis, isFirstAnalysis, qualifier, variations } = props;
+
   const sortedEvents = sortBy(
     analysis.events,
     (event) => {
@@ -53,8 +59,8 @@ export function Analysis({ analysis, ...props }: AnalysisProps) {
   );
 
   // use `TRK` for all components but applications
-  const qualifier =
-    props.qualifier === ComponentQualifier.Application
+  const displayedQualifier =
+    qualifier === ComponentQualifier.Application
       ? ComponentQualifier.Application
       : ComponentQualifier.Project;
 
@@ -66,7 +72,11 @@ export function Analysis({ analysis, ...props }: AnalysisProps) {
 
       {sortedEvents.length > 0
         ? sortedEvents.map((event) => <Event event={event} key={event.key} />)
-        : translate('project_activity.analyzed', qualifier)}
+        : translate('project_activity.analyzed', displayedQualifier)}
+
+      {qualifier === ComponentQualifier.Project && variations !== undefined && (
+        <AnalysisVariations isFirstAnalysis={isFirstAnalysis} variations={variations} />
+      )}
     </div>
   );
 }
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/AnalysisVariations.tsx b/server/sonar-web/src/main/js/apps/overview/branches/AnalysisVariations.tsx
new file mode 100644 (file)
index 0000000..97cfd98
--- /dev/null
@@ -0,0 +1,146 @@
+/*
+ * 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 styled from '@emotion/styled';
+import { TrendDownIcon, TrendUpIcon, themeColor } from 'design-system';
+import React from 'react';
+import { FormattedMessage } from 'react-intl';
+import { formatMeasure } from '../../../helpers/measures';
+import { MetricType } from '../../../types/metrics';
+import { AnalysisMeasuresVariations } from '../../../types/project-activity';
+
+interface AnalysisVariationsProps {
+  isFirstAnalysis?: boolean;
+  variations: AnalysisMeasuresVariations;
+}
+
+interface VariationProps {
+  isGoodIfGrowing: boolean;
+  label: string;
+  showVariationIcon?: boolean;
+  valueType?: MetricType;
+  variation?: number;
+}
+
+function Variation(props: Readonly<VariationProps>) {
+  const {
+    isGoodIfGrowing,
+    label,
+    showVariationIcon = true,
+    valueType = MetricType.Integer,
+    variation,
+  } = props;
+
+  if (variation === undefined) {
+    return null;
+  }
+
+  const formattedValue = formatMeasure(variation, valueType);
+
+  if (!showVariationIcon) {
+    return (
+      <span className="sw-flex sw-items-center sw-mx-2">
+        {formattedValue} {<FormattedMessage id={label} />}
+      </span>
+    );
+  }
+
+  let variationIcon = <EqualIconContainer className="sw-text-lg">=</EqualIconContainer>;
+
+  if (variation !== 0) {
+    const ArrowIcon = variation > 0 ? TrendUpIcon : TrendDownIcon;
+    const ArrowIconContainer =
+      variation > 0 === isGoodIfGrowing
+        ? CaYCCompliantIconContainer
+        : CaYCNonCompliantIconContainer;
+
+    variationIcon = (
+      <ArrowIconContainer>
+        <ArrowIcon width={20} />
+      </ArrowIconContainer>
+    );
+  }
+
+  const variationToDisplay = formattedValue.startsWith('-') ? formattedValue : `+${formattedValue}`;
+
+  return (
+    <span className="sw-flex sw-items-center sw-mx-1">
+      {variationIcon} {variationToDisplay} {<FormattedMessage id={label} />}
+    </span>
+  );
+}
+
+export function AnalysisVariations(props: Readonly<AnalysisVariationsProps>) {
+  const { isFirstAnalysis, variations } = props;
+
+  const issuesVariation =
+    (variations.bugs ?? 0) + (variations.code_smells ?? 0) + (variations.vulnerabilities ?? 0);
+  const coverageVariation = variations.coverage;
+  const duplicationsVariation = variations.duplicated_lines_density;
+
+  return (
+    <div className="sw-flex sw-items-center sw-mt-1">
+      <FormattedMessage
+        id={
+          isFirstAnalysis
+            ? 'overview.activity.variations.first_analysis'
+            : 'overview.activity.variations.new_analysis'
+        }
+      />
+      <Variation
+        isGoodIfGrowing={false}
+        label="project_activity.graphs.issues"
+        showVariationIcon={!isFirstAnalysis}
+        variation={issuesVariation}
+      />
+      {coverageVariation !== undefined && <SeparatorContainer>&bull;</SeparatorContainer>}
+      <Variation
+        isGoodIfGrowing
+        label="project_activity.graphs.coverage"
+        showVariationIcon={!isFirstAnalysis}
+        valueType={MetricType.Percent}
+        variation={coverageVariation}
+      />
+      {duplicationsVariation !== undefined && <SeparatorContainer>&bull;</SeparatorContainer>}
+      <Variation
+        isGoodIfGrowing={false}
+        label="project_activity.graphs.duplications"
+        showVariationIcon={!isFirstAnalysis}
+        valueType={MetricType.Percent}
+        variation={duplicationsVariation}
+      />
+    </div>
+  );
+}
+
+const CaYCCompliantIconContainer = styled.span`
+  color: ${themeColor('iconSuccess')};
+`;
+
+const CaYCNonCompliantIconContainer = styled.span`
+  color: ${themeColor('iconError')};
+`;
+
+const EqualIconContainer = styled.span`
+  color: ${themeColor('iconInfo')};
+`;
+
+const SeparatorContainer = styled.span`
+  color: ${themeColor('iconStatus')};
+`;
index 96a7717ac29c1ca30ce3a0ce0cdc2c212daf4e8a..0c02104405e11b7839386067430ed21b9a6cc27e 100644 (file)
@@ -23,11 +23,14 @@ import { mockComponent } from '../../../../helpers/mocks/component';
 import {
   mockAnalysis,
   mockAnalysisEvent,
+  mockHistoryItem,
   mockMeasureHistory,
 } from '../../../../helpers/mocks/project-activity';
 import { mockMetric } from '../../../../helpers/testMocks';
 
+import { parseDate } from '../../../../helpers/dates';
 import { renderComponent } from '../../../../helpers/testReactTestingUtils';
+import { MetricKey } from '../../../../types/metrics';
 import {
   ApplicationAnalysisEventCategory,
   DefinitionChangeType,
@@ -42,10 +45,74 @@ it('should render correctly', async () => {
   expect(screen.getByText(/event.category.OTHER/)).toBeInTheDocument();
   expect(screen.getByText(/event.category.DEFINITION_CHANGE/)).toBeInTheDocument();
   expect(screen.getByText('event.sqUpgrade10.2')).toBeInTheDocument();
+
+  // Checking measures variations
+  expect(screen.getAllByText(/project_activity\.graphs\.coverage$/)).toHaveLength(3);
+  expect(screen.getAllByText(/project_activity\.graphs\.duplications$/)).toHaveLength(3);
+  // Analysis 1 (latest)
+  expect(screen.getByText(/^\+0 project_activity\.graphs\.issues$/)).toBeInTheDocument();
+  expect(screen.getByText(/^\+6\.5% project_activity\.graphs\.duplications$/)).toBeInTheDocument();
+  // Analysis 2
+  expect(screen.getByText(/^\+2 project_activity\.graphs\.issues$/)).toBeInTheDocument();
+  expect(screen.getByText(/^-1\.0% project_activity\.graphs\.coverage$/)).toBeInTheDocument();
+  // Analysis 3
+  expect(screen.getByText(/^-100 project_activity\.graphs\.issues$/)).toBeInTheDocument();
+  expect(screen.getByText(/^\+15\.2% project_activity\.graphs\.coverage$/)).toBeInTheDocument();
+  expect(screen.getByText(/^-1\.5% project_activity\.graphs\.duplications$/)).toBeInTheDocument();
+  // Analysis 4 (first one)
+  expect(screen.getByText(/^502 project_activity\.graphs\.issues$/)).toBeInTheDocument();
+  expect(screen.getByText(/^0\.0% project_activity\.graphs\.coverage$/)).toBeInTheDocument();
+  expect(screen.getByText(/^10\.0% project_activity\.graphs\.duplications$/)).toBeInTheDocument();
 });
 
 function renderActivityPanel(props: Partial<ActivityPanelProps> = {}) {
-  const mockedMeasureHistory = [mockMeasureHistory()];
+  const mockedMeasureHistory = [
+    mockMeasureHistory({
+      metric: MetricKey.code_smells,
+      history: [
+        mockHistoryItem({ date: parseDate('2018-10-27T10:21:15+0200'), value: '500' }),
+        mockHistoryItem({ date: parseDate('2018-10-27T12:21:15+0200'), value: '400' }),
+        mockHistoryItem({ date: parseDate('2020-10-27T16:33:50+0200'), value: '400' }),
+        mockHistoryItem({ date: parseDate('2020-10-27T18:33:50+0200'), value: '400' }),
+      ],
+    }),
+    mockMeasureHistory({
+      metric: MetricKey.bugs,
+      history: [
+        mockHistoryItem({ date: parseDate('2018-10-27T10:21:15+0200'), value: '0' }),
+        mockHistoryItem({ date: parseDate('2018-10-27T12:21:15+0200'), value: '0' }),
+        mockHistoryItem({ date: parseDate('2020-10-27T16:33:50+0200'), value: '2' }),
+        mockHistoryItem({ date: parseDate('2020-10-27T18:33:50+0200'), value: '0' }),
+      ],
+    }),
+    mockMeasureHistory({
+      metric: MetricKey.vulnerabilities,
+      history: [
+        mockHistoryItem({ date: parseDate('2018-10-27T10:21:15+0200'), value: '2' }),
+        mockHistoryItem({ date: parseDate('2018-10-27T12:21:15+0200'), value: '2' }),
+        mockHistoryItem({ date: parseDate('2020-10-27T16:33:50+0200'), value: '2' }),
+        mockHistoryItem({ date: parseDate('2020-10-27T18:33:50+0200'), value: '4' }),
+      ],
+    }),
+    mockMeasureHistory({
+      metric: MetricKey.duplicated_lines_density,
+      history: [
+        mockHistoryItem({ date: parseDate('2018-10-27T10:21:15+0200'), value: '10.0' }),
+        mockHistoryItem({ date: parseDate('2018-10-27T12:21:15+0200'), value: '8.5' }),
+        mockHistoryItem({ date: parseDate('2020-10-27T16:33:50+0200'), value: '8.5' }),
+        mockHistoryItem({ date: parseDate('2020-10-27T18:33:50+0200'), value: '15.0' }),
+      ],
+    }),
+    mockMeasureHistory({
+      metric: MetricKey.coverage,
+      history: [
+        mockHistoryItem({ date: parseDate('2018-10-27T10:21:15+0200'), value: '0.0' }),
+        mockHistoryItem({ date: parseDate('2018-10-27T12:21:15+0200'), value: '15.2' }),
+        mockHistoryItem({ date: parseDate('2020-10-27T16:33:50+0200'), value: '14.2' }),
+        mockHistoryItem({ date: parseDate('2020-10-27T18:33:50+0200'), value: '14.2' }),
+      ],
+    }),
+  ];
   const mockedMetrics = [mockMetric()];
   const mockedAnalysis = [
     mockAnalysis({
@@ -83,6 +150,8 @@ function renderActivityPanel(props: Partial<ActivityPanelProps> = {}) {
       ],
     }),
     mockAnalysis({ key: 'bar' }),
+    mockAnalysis(),
+    mockAnalysis(),
   ];
 
   const mockedProps: ActivityPanelProps = {
index e42daa229315d3d56d48fa9e4e652ad1df4ca432..195386a2a1aea695959b5b6fe7896351e8a4099f 100644 (file)
@@ -25,6 +25,7 @@ import { translate } from '../../helpers/l10n';
 import { parseAsString } from '../../helpers/query';
 import { IssueType } from '../../types/issues';
 import { MetricKey } from '../../types/metrics';
+import { AnalysisMeasuresVariations, MeasureHistory } from '../../types/project-activity';
 import { RawQuery } from '../../types/types';
 
 export const METRICS: string[] = [
@@ -107,6 +108,14 @@ export const HISTORY_METRICS_LIST: string[] = [
   MetricKey.coverage,
 ];
 
+const MEASURES_VARIATIONS_METRICS = [
+  MetricKey.bugs,
+  MetricKey.code_smells,
+  MetricKey.coverage,
+  MetricKey.duplicated_lines_density,
+  MetricKey.vulnerabilities,
+];
+
 export enum MeasurementType {
   Coverage = 'COVERAGE',
   Duplication = 'DUPLICATION',
@@ -187,3 +196,39 @@ export const parseQuery = memoize((urlQuery: RawQuery): { codeScope: string } =>
     codeScope: parseAsString(urlQuery['code_scope']),
   };
 });
+
+export function getAnalysisVariations(measures: MeasureHistory[], analysesCount: number) {
+  if (analysesCount === 0) {
+    return [];
+  }
+
+  const emptyVariations: AnalysisMeasuresVariations[] = Array.from(
+    { length: analysesCount },
+    () => ({}),
+  );
+
+  return measures.reduce((variations, { metric, history }) => {
+    if (!MEASURES_VARIATIONS_METRICS.includes(metric)) {
+      return variations;
+    }
+
+    history.slice(-analysesCount).forEach(({ value = '' }, index, analysesHistory) => {
+      if (index === 0) {
+        variations[index][metric] = parseFloat(value) || 0;
+        return;
+      }
+
+      const previousValue = parseFloat(analysesHistory[index - 1].value ?? '') || 0;
+      const numericValue = parseFloat(value) || 0;
+      const variation = numericValue - previousValue;
+
+      if (variation === 0) {
+        return;
+      }
+
+      variations[index][metric] = variation;
+    });
+
+    return variations;
+  }, emptyVariations);
+}
index e9a46c4a7fef55be33d5112601a4cc457e48ceb7..ecd36935267502b29cd1de7d11b0872eb7a77bd1 100644 (file)
@@ -78,18 +78,20 @@ function renderGraphsTooltips(props: Partial<Props> = {}) {
   const date = props.selectedDate || parseDate('2016-01-01T00:00:00+0200');
   const metrics: Metric[] = [];
 
-  [
-    [MetricKey.bugs, '1'],
-    [MetricKey.reliability_rating, '3'],
-    [MetricKey.code_smells, '0'],
-    [MetricKey.sqale_rating, '1'],
-    [MetricKey.vulnerabilities, '2'],
-    [MetricKey.security_rating, '5'],
-    [MetricKey.lines_to_cover, '10'],
-    [MetricKey.uncovered_lines, '8'],
-    [MetricKey.coverage, '75'],
-    [MetricKey.duplicated_lines_density, '3'],
-  ].forEach(([metric, value]) => {
+  (
+    [
+      [MetricKey.bugs, '1'],
+      [MetricKey.reliability_rating, '3'],
+      [MetricKey.code_smells, '0'],
+      [MetricKey.sqale_rating, '1'],
+      [MetricKey.vulnerabilities, '2'],
+      [MetricKey.security_rating, '5'],
+      [MetricKey.lines_to_cover, '10'],
+      [MetricKey.uncovered_lines, '8'],
+      [MetricKey.coverage, '75'],
+      [MetricKey.duplicated_lines_density, '3'],
+    ] as Array<[MetricKey, string]>
+  ).forEach(([metric, value]) => {
     measuresHistory.push(
       mockMeasureHistory({
         metric,
index 55d66388e089a7dcbf57983db321ea301c60a142..1a82a555ea097353928b466d5b0bc1528b4f39ec 100644 (file)
@@ -17,6 +17,7 @@
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { MetricKey } from './metrics';
 import { Status } from './types';
 
 interface BaseAnalysis {
@@ -91,7 +92,7 @@ export interface HistoryItem {
 }
 
 export interface MeasureHistory {
-  metric: string;
+  metric: MetricKey;
   history: HistoryItem[];
 }
 
@@ -106,3 +107,5 @@ export interface Point {
   x: Date;
   y: number | string | undefined;
 }
+
+export type AnalysisMeasuresVariations = Partial<Record<MetricKey, number>>;
index d7cce749480993afc3fd9f9c5954cc2afa61b0c7..b6c166ee70155956ea246e8d45a1151845b42ac8 100644 (file)
@@ -3879,6 +3879,9 @@ overview.badges.renew.description=If your project badge security token has leake
 overview.quality_profiles_update_after_sq_upgrade.message=Upgrade to SonarQube {sqVersion} has updated your Quality Profiles. Issues on your project may have been affected. {link}
 overview.quality_profiles_update_after_sq_upgrade.link=See more details
 
+overview.activity.variations.new_analysis=New analysis:
+overview.activity.variations.first_analysis=First analysis:
+
 
 #------------------------------------------------------------------------------
 #