]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-22716 Combine old ratings and new ones in one activity graph
authorViktor Vorona <viktor.vorona@sonarsource.com>
Wed, 21 Aug 2024 15:34:53 +0000 (17:34 +0200)
committersonartech <sonartech@sonarsource.com>
Mon, 26 Aug 2024 20:03:07 +0000 (20:03 +0000)
20 files changed:
server/sonar-web/src/main/js/api/mocks/TimeMachineServiceMock.ts
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityApp.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityAppRenderer.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-it.tsx
server/sonar-web/src/main/js/apps/projectActivity/utils.ts
server/sonar-web/src/main/js/components/activity-graph/AddGraphMetric.tsx
server/sonar-web/src/main/js/components/activity-graph/GraphHistory.tsx
server/sonar-web/src/main/js/components/activity-graph/GraphsHistory.tsx
server/sonar-web/src/main/js/components/charts/AdvancedTimeline.tsx
server/sonar-web/src/main/js/components/charts/SplitLine.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/charts/SplitLinePopover.tsx [new file with mode: 0644]
server/sonar-web/src/main/js/components/charts/__tests__/AdvancedTimeline-test.tsx
server/sonar-web/src/main/js/components/charts/__tests__/__snapshots__/AdvancedTimeline-test.tsx.snap
server/sonar-web/src/main/js/helpers/activity-graph.ts [new file with mode: 0644]
server/sonar-web/src/main/js/helpers/mocks/metrics.ts
server/sonar-web/src/main/js/queries/component.ts
server/sonar-web/src/main/js/queries/measures.ts
server/sonar-web/src/main/js/types/project-activity.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 53065863d8dbeb46af5c6eec8beaaba9aeeb06c5..c292c70ebc42333cf37c19580749265685c29864 100644 (file)
@@ -57,6 +57,7 @@ const defaultMeasureHistory = [
 
 export class TimeMachineServiceMock {
   #measureHistory: MeasureHistory[];
+  toISO = false;
 
   constructor() {
     this.#measureHistory = cloneDeep(defaultMeasureHistory);
@@ -109,7 +110,10 @@ export class TimeMachineServiceMock {
   map = (list: MeasureHistory[]) => {
     return list.map((item) => ({
       ...item,
-      history: item.history.map((h) => ({ ...h, date: h.date.toDateString() })),
+      history: item.history.map((h) => ({
+        ...h,
+        date: this.toISO ? h.date.toISOString() : h.date.toDateString(),
+      })),
     }));
   };
 
index 0b0b03406207940f5b35ec42c4f8d4ee01e462b5..f4cb3432bb31c6454b91eb0f4344207866fa270d 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 { Spinner } from '@sonarsource/echoes-react';
 import React from 'react';
 import { useLocation, useRouter } from '~sonar-aligned/components/hoc/withRouter';
 import { getBranchLikeQuery } from '~sonar-aligned/helpers/branch-like';
@@ -33,11 +34,14 @@ import {
   getHistoryMetrics,
   isCustomGraph,
 } from '../../../components/activity-graph/utils';
+import { mergeRatingMeasureHistory } from '../../../helpers/activity-graph';
+import { SOFTWARE_QUALITY_RATING_METRICS } from '../../../helpers/constants';
 import { parseDate } from '../../../helpers/dates';
 import useApplicationLeakQuery from '../../../queries/applications';
 import { useBranchesQuery } from '../../../queries/branch';
 import { useAllMeasuresHistoryQuery } from '../../../queries/measures';
 import { useAllProjectAnalysesQuery } from '../../../queries/project-analyses';
+import { useIsLegacyCCTMode } from '../../../queries/settings';
 import { isApplication, isProject } from '../../../types/component';
 import { MeasureHistory, ParsedAnalysis } from '../../../types/project-activity';
 import { Query, parseQuery, serializeUrlQuery } from '../utils';
@@ -73,26 +77,22 @@ export function ProjectActivityApp() {
   );
 
   const { data: analysesData, isLoading: isLoadingAnalyses } = useAllProjectAnalysesQuery(enabled);
+  const { data: isLegacy, isLoading: isLoadingLegacy } = useIsLegacyCCTMode();
 
   const { data: historyData, isLoading: isLoadingHistory } = useAllMeasuresHistoryQuery(
-    componentKey,
-    getBranchLikeQuery(branchLike),
-    getHistoryMetrics(query.graph || DEFAULT_GRAPH, parsedQuery.customMetrics).join(','),
-    enabled,
+    {
+      component: componentKey,
+      branchParams: getBranchLikeQuery(branchLike),
+      metrics: getHistoryMetrics(query.graph || DEFAULT_GRAPH, parsedQuery.customMetrics).join(','),
+    },
+    { enabled },
   );
 
   const analyses = React.useMemo(() => analysesData ?? [], [analysesData]);
 
   const measuresHistory = React.useMemo(
-    () =>
-      historyData?.measures?.map((measure) => ({
-        metric: measure.metric,
-        history: measure.history.map((historyItem) => ({
-          date: parseDate(historyItem.date),
-          value: historyItem.value,
-        })),
-      })) ?? [],
-    [historyData],
+    () => (isLoadingLegacy ? [] : mergeRatingMeasureHistory(historyData, parseDate, isLegacy)),
+    [historyData, isLegacy, isLoadingLegacy],
   );
 
   const leakPeriodDate = React.useMemo(() => {
@@ -137,20 +137,31 @@ export function ProjectActivityApp() {
     });
   };
 
+  const firstSoftwareQualityRatingMetric = historyData?.measures.find((m) =>
+    SOFTWARE_QUALITY_RATING_METRICS.includes(m.metric),
+  );
+
   return (
     component && (
-      <ProjectActivityAppRenderer
-        analyses={analyses}
-        analysesLoading={isLoadingAnalyses}
-        graphLoading={isLoadingHistory}
-        leakPeriodDate={leakPeriodDate}
-        initializing={isLoadingAnalyses || isLoadingHistory}
-        measuresHistory={measuresHistory}
-        metrics={filteredMetrics}
-        project={component}
-        onUpdateQuery={handleUpdateQuery}
-        query={parsedQuery}
-      />
+      <Spinner isLoading={isLoadingLegacy}>
+        <ProjectActivityAppRenderer
+          analyses={analyses}
+          isLegacy={
+            isLegacy ||
+            !firstSoftwareQualityRatingMetric ||
+            firstSoftwareQualityRatingMetric.history.every((h) => h.value === undefined)
+          }
+          analysesLoading={isLoadingAnalyses}
+          graphLoading={isLoadingHistory}
+          leakPeriodDate={leakPeriodDate}
+          initializing={isLoadingAnalyses || isLoadingHistory}
+          measuresHistory={measuresHistory}
+          metrics={filteredMetrics}
+          project={component}
+          onUpdateQuery={handleUpdateQuery}
+          query={parsedQuery}
+        />
+      </Spinner>
     )
   );
 }
index 6feb7c9302c2b7ed52aa190ea107913223691600..f52fbbd5b7273919a68a1d2d536ce57a893fe4f5 100644 (file)
@@ -42,6 +42,7 @@ interface Props {
   analysesLoading: boolean;
   graphLoading: boolean;
   initializing: boolean;
+  isLegacy?: boolean;
   leakPeriodDate?: Date;
   measuresHistory: MeasureHistory[];
   metrics: Metric[];
@@ -61,6 +62,7 @@ export default function ProjectActivityAppRenderer(props: Props) {
     graphLoading,
     metrics,
     project,
+    isLegacy,
   } = props;
   const { configuration, qualifier } = props.project;
   const canAdmin =
@@ -101,6 +103,7 @@ export default function ProjectActivityAppRenderer(props: Props) {
                 analyses={analyses}
                 leakPeriodDate={leakPeriodDate}
                 loading={graphLoading}
+                isLegacy={isLegacy}
                 measuresHistory={measuresHistory}
                 metrics={metrics}
                 project={project.key}
index a406dbd8d0e2203ae557b206ce8a4cd82a9da924..80b1d177e9271358c45871c22667acdac84c85bf 100644 (file)
@@ -35,9 +35,13 @@ import {
   splitSeriesInGraphs,
 } from '../../../components/activity-graph/utils';
 import DocumentationLink from '../../../components/common/DocumentationLink';
-import { CCT_SOFTWARE_QUALITY_METRICS } from '../../../helpers/constants';
+import {
+  CCT_SOFTWARE_QUALITY_METRICS,
+  SOFTWARE_QUALITY_RATING_METRICS_MAP,
+} from '../../../helpers/constants';
 import { DocLink } from '../../../helpers/doc-links';
 import { translate } from '../../../helpers/l10n';
+import { MetricKey } from '../../../sonar-aligned/types/metrics';
 import {
   GraphType,
   MeasureHistory,
@@ -51,6 +55,7 @@ import { PROJECT_ACTIVITY_GRAPH } from './ProjectActivityApp';
 
 interface Props {
   analyses: ParsedAnalysis[];
+  isLegacy?: boolean;
   leakPeriodDate?: Date;
   loading: boolean;
   measuresHistory: MeasureHistory[];
@@ -205,25 +210,28 @@ export default class ProjectActivityGraphs extends React.PureComponent<Props, St
     }
   };
 
+  hasGaps = (value?: MeasureHistory) => {
+    const indexOfFirstMeasureWithValue = value?.history.findIndex((item) => item.value);
+
+    return indexOfFirstMeasureWithValue === -1
+      ? false
+      : value?.history.slice(indexOfFirstMeasureWithValue).some((item) => item.value === undefined);
+  };
+
   renderQualitiesMetricInfoMessage = () => {
-    const { measuresHistory } = this.props;
+    const { measuresHistory, isLegacy } = this.props;
 
     const qualityMeasuresHistory = measuresHistory.find((history) =>
       CCT_SOFTWARE_QUALITY_METRICS.includes(history.metric),
     );
-
-    const indexOfFirstMeasureWithValue = qualityMeasuresHistory?.history.findIndex(
-      (item) => item.value,
+    const ratingQualityMeasuresHistory = measuresHistory.find((history) =>
+      (Object.keys(SOFTWARE_QUALITY_RATING_METRICS_MAP) as MetricKey[]).includes(history.metric),
     );
 
-    const hasGaps =
-      indexOfFirstMeasureWithValue === -1
-        ? false
-        : qualityMeasuresHistory?.history
-            .slice(indexOfFirstMeasureWithValue)
-            .some((item) => item.value === undefined);
-
-    if (hasGaps) {
+    if (
+      this.hasGaps(qualityMeasuresHistory) ||
+      (!isLegacy && this.hasGaps(ratingQualityMeasuresHistory))
+    ) {
       return (
         <FlagMessage variant="info">
           <FormattedMessage
@@ -245,7 +253,8 @@ export default class ProjectActivityGraphs extends React.PureComponent<Props, St
   };
 
   render() {
-    const { analyses, leakPeriodDate, loading, measuresHistory, metrics, query } = this.props;
+    const { analyses, leakPeriodDate, loading, measuresHistory, metrics, query, isLegacy } =
+      this.props;
     const { graphEndDate, graphStartDate, series } = this.state;
 
     return (
@@ -269,6 +278,7 @@ export default class ProjectActivityGraphs extends React.PureComponent<Props, St
           graphs={this.state.graphs}
           leakPeriodDate={leakPeriodDate}
           loading={loading}
+          isLegacy={isLegacy}
           measuresHistory={measuresHistory}
           removeCustomMetric={this.handleRemoveCustomMetric}
           selectedDate={query.selectedDate}
index aa54fa9174317647047dff9a9e292d7f5e315071..4cf9ca58475b1f61165ea4b131d1abde21bf6e32 100644 (file)
@@ -27,6 +27,7 @@ import { ComponentQualifier } from '~sonar-aligned/types/component';
 import { MetricKey, MetricType } from '~sonar-aligned/types/metrics';
 import ApplicationServiceMock from '../../../../api/mocks/ApplicationServiceMock';
 import { ProjectActivityServiceMock } from '../../../../api/mocks/ProjectActivityServiceMock';
+import SettingsServiceMock from '../../../../api/mocks/SettingsServiceMock';
 import { TimeMachineServiceMock } from '../../../../api/mocks/TimeMachineServiceMock';
 import { mockBranchList } from '../../../../api/mocks/data/branches';
 import { DEPRECATED_ACTIVITY_METRICS } from '../../../../helpers/constants';
@@ -46,10 +47,10 @@ import {
   GraphType,
   ProjectAnalysisEventCategory,
 } from '../../../../types/project-activity';
+import { SettingsKey } from '../../../../types/settings';
 import ProjectActivityAppContainer from '../ProjectActivityApp';
 
 jest.mock('../../../../api/projectActivity');
-jest.mock('../../../../api/time-machine');
 
 jest.mock('../../../../helpers/storage', () => ({
   ...jest.requireActual('../../../../helpers/storage'),
@@ -67,6 +68,7 @@ jest.mock('../../../../api/branches', () => ({
 const applicationHandler = new ApplicationServiceMock();
 const projectActivityHandler = new ProjectActivityServiceMock();
 const timeMachineHandler = new TimeMachineServiceMock();
+const settingsHandler = new SettingsServiceMock();
 
 let isBranchReady = false;
 
@@ -77,6 +79,7 @@ beforeEach(() => {
   applicationHandler.reset();
   projectActivityHandler.reset();
   timeMachineHandler.reset();
+  settingsHandler.reset();
 
   timeMachineHandler.setMeasureHistory(
     [
@@ -549,6 +552,204 @@ describe('graph interactions', () => {
   });
 });
 
+describe('ratings', () => {
+  it('should combine old and new rating + gaps', async () => {
+    timeMachineHandler.setMeasureHistory([
+      mockMeasureHistory({
+        metric: MetricKey.reliability_rating,
+        history: [
+          mockHistoryItem({
+            value: '5',
+            date: new Date('2022-01-11'),
+          }),
+          mockHistoryItem({
+            value: '2',
+            date: new Date('2022-01-12'),
+          }),
+          mockHistoryItem({
+            value: '2',
+            date: new Date('2022-01-13'),
+          }),
+          mockHistoryItem({
+            value: '2',
+            date: new Date('2022-01-14'),
+          }),
+        ],
+      }),
+      mockMeasureHistory({
+        metric: MetricKey.software_quality_reliability_rating,
+        history: [
+          mockHistoryItem({
+            value: undefined,
+            date: new Date('2022-01-11'),
+          }),
+          mockHistoryItem({
+            value: '3',
+            date: new Date('2022-01-12'),
+          }),
+          mockHistoryItem({
+            value: undefined,
+            date: new Date('2022-01-13'),
+          }),
+          mockHistoryItem({
+            value: '3',
+            date: new Date('2022-01-14'),
+          }),
+        ],
+      }),
+    ]);
+    const { ui } = getPageObject();
+    renderProjectActivityAppContainer();
+
+    await ui.changeGraphType(GraphType.custom);
+    await ui.openMetricsDropdown();
+    await ui.toggleMetric(MetricKey.reliability_rating);
+    await ui.closeMetricsDropdown();
+
+    expect(await ui.graphs.findAll()).toHaveLength(1);
+    expect(ui.metricChangedInfoBtn.get()).toBeInTheDocument();
+    expect(ui.gapInfoMessage.get()).toBeInTheDocument();
+    expect(byText('E').query()).not.toBeInTheDocument();
+  });
+
+  it('should not show old rating if new one was always there', async () => {
+    timeMachineHandler.setMeasureHistory([
+      mockMeasureHistory({
+        metric: MetricKey.reliability_rating,
+        history: [
+          mockHistoryItem({
+            value: '5',
+            date: new Date('2022-01-11'),
+          }),
+          mockHistoryItem({
+            value: '2',
+            date: new Date('2022-01-12'),
+          }),
+        ],
+      }),
+      mockMeasureHistory({
+        metric: MetricKey.software_quality_reliability_rating,
+        history: [
+          mockHistoryItem({
+            value: '4',
+            date: new Date('2022-01-11'),
+          }),
+          mockHistoryItem({
+            value: '3',
+            date: new Date('2022-01-12'),
+          }),
+        ],
+      }),
+    ]);
+    const { ui } = getPageObject();
+    renderProjectActivityAppContainer();
+
+    await ui.changeGraphType(GraphType.custom);
+    await ui.openMetricsDropdown();
+    await ui.toggleMetric(MetricKey.reliability_rating);
+    await ui.closeMetricsDropdown();
+
+    expect(await ui.graphs.findAll()).toHaveLength(1);
+    expect(ui.metricChangedInfoBtn.query()).not.toBeInTheDocument();
+    expect(ui.gapInfoMessage.query()).not.toBeInTheDocument();
+    expect(byText('E').query()).not.toBeInTheDocument();
+  });
+
+  it('should show E if no new metrics', async () => {
+    timeMachineHandler.setMeasureHistory([
+      mockMeasureHistory({
+        metric: MetricKey.reliability_rating,
+        history: [
+          mockHistoryItem({
+            value: '5',
+            date: new Date('2022-01-11'),
+          }),
+          mockHistoryItem({
+            value: '2',
+            date: new Date('2022-01-12'),
+          }),
+          mockHistoryItem({
+            value: '2',
+            date: new Date('2022-01-13'),
+          }),
+        ],
+      }),
+    ]);
+    const { ui } = getPageObject();
+    renderProjectActivityAppContainer();
+
+    await ui.changeGraphType(GraphType.custom);
+    await ui.openMetricsDropdown();
+    await ui.toggleMetric(MetricKey.reliability_rating);
+    await ui.closeMetricsDropdown();
+
+    expect(await ui.graphs.findAll()).toHaveLength(1);
+    expect(ui.metricChangedInfoBtn.query()).not.toBeInTheDocument();
+    expect(ui.gapInfoMessage.query()).not.toBeInTheDocument();
+    expect(byText('E').get()).toBeInTheDocument();
+  });
+
+  it('should not show gaps message and metric change button, but should show E in legacy mode', async () => {
+    settingsHandler.set(SettingsKey.LegacyMode, 'true');
+    timeMachineHandler.setMeasureHistory([
+      mockMeasureHistory({
+        metric: MetricKey.reliability_rating,
+        history: [
+          mockHistoryItem({
+            value: '5',
+            date: new Date('2022-01-11'),
+          }),
+          mockHistoryItem({
+            value: '2',
+            date: new Date('2022-01-12'),
+          }),
+          mockHistoryItem({
+            value: '2',
+            date: new Date('2022-01-13'),
+          }),
+          mockHistoryItem({
+            value: '2',
+            date: new Date('2022-01-14'),
+          }),
+        ],
+      }),
+      mockMeasureHistory({
+        metric: MetricKey.software_quality_reliability_rating,
+        history: [
+          mockHistoryItem({
+            value: undefined,
+            date: new Date('2022-01-11'),
+          }),
+          mockHistoryItem({
+            value: '4',
+            date: new Date('2022-01-12'),
+          }),
+          mockHistoryItem({
+            value: undefined,
+            date: new Date('2022-01-13'),
+          }),
+          mockHistoryItem({
+            value: '3',
+            date: new Date('2022-01-14'),
+          }),
+        ],
+      }),
+    ]);
+    const { ui } = getPageObject();
+    renderProjectActivityAppContainer();
+
+    await ui.changeGraphType(GraphType.custom);
+    await ui.openMetricsDropdown();
+    await ui.toggleMetric(MetricKey.reliability_rating);
+    await ui.closeMetricsDropdown();
+
+    expect(await ui.graphs.findAll()).toHaveLength(1);
+    expect(ui.metricChangedInfoBtn.query()).not.toBeInTheDocument();
+    expect(ui.gapInfoMessage.query()).not.toBeInTheDocument();
+    expect(byText('E').get()).toBeInTheDocument();
+  });
+});
+
 function getPageObject() {
   const user = userEvent.setup();
 
@@ -562,6 +763,9 @@ function getPageObject() {
     graphs: byLabelText('project_activity.graphs.explanation_x', { exact: false }),
     noDataText: byText('project_activity.graphs.custom.no_history'),
     gapInfoMessage: byText('project_activity.graphs.data_table.data_gap', { exact: false }),
+    metricChangedInfoBtn: byRole('button', {
+      name: 'project_activity.graphs.rating_split.info_icon',
+    }),
 
     // Add metrics.
     addMetricBtn: byRole('button', { name: 'project_activity.graphs.custom.add' }),
@@ -623,7 +827,7 @@ function getPageObject() {
       },
 
       async changeGraphType(type: GraphType) {
-        await user.click(ui.graphTypeSelect.get());
+        await user.click(await ui.graphTypeSelect.find());
         const optionForType = await screen.findByText(`project_activity.graphs.${type}`);
         await user.click(optionForType);
       },
@@ -759,6 +963,7 @@ function renderProjectActivityAppContainer(
           mockMetric({ key: MetricKey.code_smells, type: MetricType.Integer }),
           mockMetric({ key: MetricKey.security_hotspots_reviewed }),
           mockMetric({ key: MetricKey.security_review_rating, type: MetricType.Rating }),
+          mockMetric({ key: MetricKey.reliability_rating, type: MetricType.Rating }),
         ],
         'key',
       ),
index 93e6703bfc24fb768241a5e7b59204f36902f50f..95bde09ef29a5062fd2fdfaf36def569567f9f0a 100644 (file)
@@ -22,6 +22,7 @@ import { isEqual, uniq } from 'lodash';
 import { MetricKey } from '~sonar-aligned/types/metrics';
 import { RawQuery } from '~sonar-aligned/types/router';
 import { DEFAULT_GRAPH } from '../../components/activity-graph/utils';
+import { SOFTWARE_QUALITY_RATING_METRICS_MAP } from '../../helpers/constants';
 import { parseDate } from '../../helpers/dates';
 import { MEASURES_REDIRECTION } from '../../helpers/measures';
 import {
@@ -113,7 +114,21 @@ export function getAnalysesByVersionByDay(
 
 export function parseQuery(urlQuery: RawQuery): Query {
   const parsedMetrics = parseAsArray(urlQuery['custom_metrics'], parseAsString<MetricKey>);
-  const customMetrics = uniq(parsedMetrics.map((metric) => MEASURES_REDIRECTION[metric] ?? metric));
+  let customMetrics = uniq(parsedMetrics.map((metric) => MEASURES_REDIRECTION[metric] ?? metric));
+
+  const reversedMetricMap = Object.fromEntries(
+    Object.entries(SOFTWARE_QUALITY_RATING_METRICS_MAP).map(
+      ([k, v]) => [v, k] as [MetricKey, MetricKey],
+    ),
+  );
+
+  customMetrics = uniq(customMetrics.map((metric) => reversedMetricMap[metric] ?? metric))
+    .map((metric) =>
+      SOFTWARE_QUALITY_RATING_METRICS_MAP[metric]
+        ? [metric, SOFTWARE_QUALITY_RATING_METRICS_MAP[metric]]
+        : metric,
+    )
+    .flat();
 
   return {
     category: parseAsString(urlQuery['category']),
@@ -136,7 +151,12 @@ export function serializeQuery(query: Query): RawQuery {
 export function serializeUrlQuery(query: Query): RawQuery {
   return cleanQuery({
     category: serializeString(query.category),
-    custom_metrics: serializeStringArray(query.customMetrics),
+    custom_metrics: serializeStringArray(
+      query.customMetrics.filter(
+        (metric) =>
+          !Object.values(SOFTWARE_QUALITY_RATING_METRICS_MAP).includes(metric as MetricKey),
+      ),
+    ),
     from: serializeDate(query.from),
     graph: serializeGraph(query.graph),
     id: serializeString(query.project),
index d8dda386b1205188438b542c9c735036ef642313..b79add8c0f4506c2123145dc4ffd906b1ad7fa5e 100644 (file)
@@ -23,7 +23,11 @@ import { Dropdown, TextMuted } from 'design-system';
 import { sortBy } from 'lodash';
 import * as React from 'react';
 import { MetricKey, MetricType } from '~sonar-aligned/types/metrics';
-import { CCT_SOFTWARE_QUALITY_METRICS, HIDDEN_METRICS } from '../../helpers/constants';
+import {
+  CCT_SOFTWARE_QUALITY_METRICS,
+  HIDDEN_METRICS,
+  SOFTWARE_QUALITY_RATING_METRICS_MAP,
+} from '../../helpers/constants';
 import { getLocalizedMetricName, translate } from '../../helpers/l10n';
 import { isDiffMetric } from '../../helpers/measures';
 import { Metric } from '../../types/types';
@@ -77,6 +81,9 @@ export default class AddGraphMetric extends React.PureComponent<Props, State> {
         if (HIDDEN_METRICS.includes(metric.key as MetricKey)) {
           return false;
         }
+        if (Object.values(SOFTWARE_QUALITY_RATING_METRICS_MAP).includes(metric.key as MetricKey)) {
+          return false;
+        }
         if (
           selectedMetrics.includes(metric.key) ||
           !getLocalizedMetricName(metric).toLowerCase().includes(query.toLowerCase())
@@ -93,7 +100,11 @@ export default class AddGraphMetric extends React.PureComponent<Props, State> {
 
   getSelectedMetricsElements = (metrics: Metric[], selectedMetrics: string[]) => {
     return metrics
-      .filter((metric) => selectedMetrics.includes(metric.key))
+      .filter(
+        (metric) =>
+          selectedMetrics.includes(metric.key) &&
+          !Object.values(SOFTWARE_QUALITY_RATING_METRICS_MAP).includes(metric.key as MetricKey),
+      )
       .map((metric) => metric.key);
   };
 
index dd5510c9501f85a191c5b6fe4a28afaeb51357b0..028c1ccd46e1afa0ff0154143b5db1e1d7671335 100644 (file)
@@ -41,6 +41,7 @@ interface Props {
   graphEndDate?: Date;
   graphStartDate?: Date;
   isCustom?: boolean;
+  isLegacy?: boolean;
   leakPeriodDate?: Date;
   measuresHistory: MeasureHistory[];
   metricsType: string;
@@ -53,120 +54,117 @@ interface Props {
   updateTooltip: (selectedDate?: Date) => void;
 }
 
-interface State {
-  tooltipIdx?: number;
-  tooltipXPos?: number;
-}
-
-export default class GraphHistory extends React.PureComponent<Props, State> {
-  state: State = {};
+export default function GraphHistory(props: Readonly<Props>) {
+  const {
+    analyses,
+    canShowDataAsTable = true,
+    graph,
+    graphEndDate,
+    graphStartDate,
+    isCustom,
+    leakPeriodDate,
+    measuresHistory,
+    metricsType,
+    selectedDate,
+    series,
+    showAreas,
+    graphDescription,
+    isLegacy,
+  } = props;
+  const [tooltipIdx, setTooltipIdx] = React.useState<number | undefined>(undefined);
+  const [tooltipXPos, setTooltipXPos] = React.useState<number | undefined>(undefined);
 
-  formatValue = (tick: string | number) => {
-    return formatMeasure(tick, getShortType(this.props.metricsType));
+  const formatValue = (tick: string | number) => {
+    return formatMeasure(tick, getShortType(metricsType));
   };
 
-  formatTooltipValue = (tick: string | number) => {
-    return formatMeasure(tick, this.props.metricsType);
+  const formatTooltipValue = (tick: string | number) => {
+    return formatMeasure(tick, metricsType);
   };
 
-  updateTooltip = (selectedDate?: Date, tooltipXPos?: number, tooltipIdx?: number) => {
-    this.props.updateTooltip(selectedDate);
-    this.setState({ tooltipXPos, tooltipIdx });
+  const updateTooltip = (selectedDate?: Date, tooltipXPos?: number, tooltipIdx?: number) => {
+    props.updateTooltip(selectedDate);
+    setTooltipIdx(tooltipIdx);
+    setTooltipXPos(tooltipXPos);
   };
 
-  render() {
-    const {
-      analyses,
-      canShowDataAsTable = true,
-      graph,
-      graphEndDate,
-      graphStartDate,
-      isCustom,
-      leakPeriodDate,
-      measuresHistory,
-      metricsType,
-      selectedDate,
-      series,
-      showAreas,
-      graphDescription,
-    } = this.props;
+  const modalProp = ({ onClose }: { onClose: () => void }) => (
+    <DataTableModal
+      analyses={analyses}
+      graphEndDate={graphEndDate}
+      graphStartDate={graphStartDate}
+      series={series}
+      onClose={onClose}
+    />
+  );
 
-    const modalProp = ({ onClose }: { onClose: () => void }) => (
-      <DataTableModal
-        analyses={analyses}
-        graphEndDate={graphEndDate}
-        graphStartDate={graphStartDate}
-        series={series}
-        onClose={onClose}
-      />
-    );
+  const events = getAnalysisEventsForDate(analyses, selectedDate);
 
-    const { tooltipIdx, tooltipXPos } = this.state;
-    const events = getAnalysisEventsForDate(analyses, selectedDate);
+  return (
+    <StyledGraphContainer className="sw-flex sw-flex-col sw-justify-center sw-items-stretch sw-grow sw-py-2">
+      {isCustom && props.removeCustomMetric ? (
+        <GraphsLegendCustom
+          leakPeriodDate={leakPeriodDate}
+          removeMetric={props.removeCustomMetric}
+          series={series}
+        />
+      ) : (
+        <GraphsLegendStatic leakPeriodDate={leakPeriodDate} series={series} />
+      )}
 
-    return (
-      <StyledGraphContainer className="sw-flex sw-flex-col sw-justify-center sw-items-stretch sw-grow sw-py-2">
-        {isCustom && this.props.removeCustomMetric ? (
-          <GraphsLegendCustom
-            leakPeriodDate={leakPeriodDate}
-            removeMetric={this.props.removeCustomMetric}
-            series={series}
-          />
-        ) : (
-          <GraphsLegendStatic leakPeriodDate={leakPeriodDate} series={series} />
-        )}
+      <div className="sw-flex-1">
+        <AutoSizer>
+          {({ height, width }) => (
+            <div>
+              <AdvancedTimeline
+                endDate={graphEndDate}
+                formatYTick={formatValue}
+                height={height}
+                leakPeriodDate={leakPeriodDate}
+                splitPointDate={measuresHistory.find((m) => m.splitPointDate)?.splitPointDate}
+                metricType={metricsType}
+                selectedDate={selectedDate}
+                isLegacy={isLegacy}
+                series={series}
+                showAreas={showAreas}
+                startDate={graphStartDate}
+                graphDescription={graphDescription}
+                updateSelectedDate={props.updateSelectedDate}
+                updateTooltip={updateTooltip}
+                updateZoom={props.updateGraphZoom}
+                width={width}
+              />
 
-        <div className="sw-flex-1">
-          <AutoSizer>
-            {({ height, width }) => (
-              <div>
-                <AdvancedTimeline
-                  endDate={graphEndDate}
-                  formatYTick={this.formatValue}
-                  height={height}
-                  leakPeriodDate={leakPeriodDate}
-                  metricType={metricsType}
-                  selectedDate={selectedDate}
-                  series={series}
-                  showAreas={showAreas}
-                  startDate={graphStartDate}
-                  graphDescription={graphDescription}
-                  updateSelectedDate={this.props.updateSelectedDate}
-                  updateTooltip={this.updateTooltip}
-                  updateZoom={this.props.updateGraphZoom}
-                  width={width}
-                />
-                {selectedDate !== undefined &&
-                  tooltipIdx !== undefined &&
-                  tooltipXPos !== undefined && (
-                    <GraphsTooltips
-                      events={events}
-                      formatValue={this.formatTooltipValue}
-                      graph={graph}
-                      graphWidth={width}
-                      measuresHistory={measuresHistory}
-                      selectedDate={selectedDate}
-                      series={series}
-                      tooltipIdx={tooltipIdx}
-                      tooltipPos={tooltipXPos}
-                    />
-                  )}
-              </div>
-            )}
-          </AutoSizer>
-        </div>
-        {canShowDataAsTable && (
-          <ModalButton modal={modalProp}>
-            {({ onClick }) => (
-              <ButtonSecondary className="sw-sr-only" onClick={onClick}>
-                {translate('project_activity.graphs.open_in_table')}
-              </ButtonSecondary>
-            )}
-          </ModalButton>
-        )}
-      </StyledGraphContainer>
-    );
-  }
+              {selectedDate !== undefined &&
+                tooltipIdx !== undefined &&
+                tooltipXPos !== undefined && (
+                  <GraphsTooltips
+                    events={events}
+                    formatValue={formatTooltipValue}
+                    graph={graph}
+                    graphWidth={width}
+                    measuresHistory={measuresHistory}
+                    selectedDate={selectedDate}
+                    series={series}
+                    tooltipIdx={tooltipIdx}
+                    tooltipPos={tooltipXPos}
+                  />
+                )}
+            </div>
+          )}
+        </AutoSizer>
+      </div>
+      {canShowDataAsTable && (
+        <ModalButton modal={modalProp}>
+          {({ onClick }) => (
+            <ButtonSecondary className="sw-sr-only" onClick={onClick}>
+              {translate('project_activity.graphs.open_in_table')}
+            </ButtonSecondary>
+          )}
+        </ModalButton>
+      )}
+    </StyledGraphContainer>
+  );
 }
 
 const StyledGraphContainer = styled.div`
index 5bf2137e1d84a5c892deafb89cd4649bddab726b..9be16362121d0c57ac0019c4cd32051fb8c855b5 100644 (file)
@@ -33,6 +33,7 @@ interface Props {
   graphEndDate?: Date;
   graphStartDate?: Date;
   graphs: Serie[][];
+  isLegacy?: boolean;
   leakPeriodDate?: Date;
   loading: boolean;
   measuresHistory: MeasureHistory[];
@@ -66,7 +67,8 @@ export default class GraphsHistory extends React.PureComponent<Props, State> {
   };
 
   render() {
-    const { analyses, graph, loading, series, ariaLabel, canShowDataAsTable } = this.props;
+    const { analyses, graph, loading, series, ariaLabel, canShowDataAsTable, isLegacy } =
+      this.props;
     const isCustom = isCustomGraph(graph);
 
     if (loading) {
@@ -105,6 +107,7 @@ export default class GraphsHistory extends React.PureComponent<Props, State> {
               graphStartDate={this.props.graphStartDate}
               isCustom={isCustom}
               key={idx}
+              isLegacy={isLegacy}
               leakPeriodDate={this.props.leakPeriodDate}
               measuresHistory={this.props.measuresHistory}
               metricsType={getSeriesMetricType(graphSeries)}
index d53df833e7bb5e9021a4f25a7a97425a95c94aef..42d786fd4db1c017126550fcc3a7fd4301779305 100644 (file)
@@ -39,6 +39,8 @@ import { Chart } from '../../types/types';
 import { LINE_CHART_DASHES } from '../activity-graph/utils';
 import './AdvancedTimeline.css';
 import './LineChart.css';
+import SplitLine from './SplitLine';
+import SplitLinePopover from './SplitLinePopover';
 
 export interface PropsWithoutTheme {
   basisCurve?: boolean;
@@ -49,6 +51,7 @@ export interface PropsWithoutTheme {
   height: number;
   hideGrid?: boolean;
   hideXAxis?: boolean;
+  isLegacy?: boolean;
   leakPeriodDate?: Date;
   // used to avoid same y ticks labels
   maxYTicksCount?: number;
@@ -57,6 +60,7 @@ export interface PropsWithoutTheme {
   selectedDate?: Date;
   series: Chart.Serie[];
   showAreas?: boolean;
+  splitPointDate?: Date;
   startDate?: Date;
   updateSelectedDate?: (selectedDate?: Date) => void;
   updateTooltip?: (selectedDate?: Date, tooltipXPos?: number, tooltipIdx?: number) => void;
@@ -141,7 +145,10 @@ export class AdvancedTimelineClass extends React.PureComponent<Props, State> {
   }
 
   getRatingScale = (availableHeight: number) => {
-    return scalePoint<number>().domain([5, 4, 3, 2, 1]).range([availableHeight, 0]);
+    const { isLegacy } = this.props;
+    return scalePoint<number>()
+      .domain(isLegacy ? [5, 4, 3, 2, 1] : [4, 3, 2, 1])
+      .range([availableHeight, 0]);
   };
 
   getLevelScale = (availableHeight: number) => {
@@ -627,7 +634,10 @@ export class AdvancedTimelineClass extends React.PureComponent<Props, State> {
       hideXAxis,
       showAreas,
       graphDescription,
+      metricType,
+      splitPointDate,
     } = this.props as PropsWithDefaults;
+    const { xScale, yScale } = this.state;
 
     if (!width || !height) {
       return <div />;
@@ -637,24 +647,36 @@ export class AdvancedTimelineClass extends React.PureComponent<Props, State> {
     const isZoomed = Boolean(startDate ?? endDate);
 
     return (
-      <svg
-        aria-label={graphDescription}
-        className={classNames('line-chart', { 'chart-zoomed': isZoomed })}
-        height={height}
-        width={width}
-      >
-        {zoomEnabled && this.renderClipPath()}
-        <g transform={`translate(${padding[3]}, ${padding[0]})`}>
-          {leakPeriodDate != null && this.renderLeak()}
-          {!hideGrid && this.renderHorizontalGrid()}
-          {!hideXAxis && this.renderXAxisTicks()}
-          {showAreas && this.renderAreas()}
-          {this.renderLines()}
-          {this.renderDots()}
-          {this.renderSelectedDate()}
-          {this.renderMouseEventsOverlay(zoomEnabled)}
-        </g>
-      </svg>
+      <div className="sw-relative">
+        <svg
+          aria-label={graphDescription}
+          className={classNames('line-chart', { 'chart-zoomed': isZoomed })}
+          height={height}
+          width={width}
+        >
+          {zoomEnabled && this.renderClipPath()}
+          <g transform={`translate(${padding[3]}, ${padding[0]})`}>
+            {leakPeriodDate != null && this.renderLeak()}
+            {!hideGrid && this.renderHorizontalGrid()}
+            {!hideXAxis && this.renderXAxisTicks()}
+            {showAreas && this.renderAreas()}
+            {this.renderLines()}
+            {this.renderDots()}
+            {this.renderSelectedDate()}
+            {this.renderMouseEventsOverlay(zoomEnabled)}
+            {metricType === MetricType.Rating && (
+              <SplitLine splitPointDate={splitPointDate} xScale={xScale} yScale={yScale} />
+            )}
+          </g>
+        </svg>
+        {metricType === MetricType.Rating && (
+          <SplitLinePopover
+            paddingLeft={padding[3]}
+            splitPointDate={splitPointDate}
+            xScale={xScale}
+          />
+        )}
+      </div>
     );
   }
 }
diff --git a/server/sonar-web/src/main/js/components/charts/SplitLine.tsx b/server/sonar-web/src/main/js/components/charts/SplitLine.tsx
new file mode 100644 (file)
index 0000000..cc65690
--- /dev/null
@@ -0,0 +1,47 @@
+/*
+ * 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 { ScaleLinear, ScalePoint, ScaleTime } from 'd3-scale';
+import * as React from 'react';
+import { shouldShowSplitLine } from '../../helpers/activity-graph';
+
+interface Props {
+  splitPointDate?: Date;
+  xScale: ScaleTime<number, number>;
+  yScale: ScaleLinear<number, number> | ScalePoint<number | string>;
+}
+
+export default function SplitLine({ splitPointDate, xScale, yScale }: Readonly<Props>) {
+  const showSplitLine = shouldShowSplitLine(splitPointDate, xScale);
+
+  if (!showSplitLine) {
+    return null;
+  }
+
+  return (
+    <line
+      className="line-tooltip"
+      strokeDasharray="2"
+      x1={xScale(splitPointDate)}
+      x2={xScale(splitPointDate)}
+      y1={yScale.range()[0]}
+      y2={yScale.range()[1] - 10}
+    />
+  );
+}
diff --git a/server/sonar-web/src/main/js/components/charts/SplitLinePopover.tsx b/server/sonar-web/src/main/js/components/charts/SplitLinePopover.tsx
new file mode 100644 (file)
index 0000000..3d29dc5
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+ * 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 { ButtonIcon, IconInfo, Popover } from '@sonarsource/echoes-react';
+import { ScaleTime } from 'd3-scale';
+import * as React from 'react';
+import { shouldShowSplitLine } from '../../helpers/activity-graph';
+import { DocLink } from '../../helpers/doc-links';
+import { translate } from '../../helpers/l10n';
+import DocumentationLink from '../common/DocumentationLink';
+
+interface Props {
+  paddingLeft: number;
+  splitPointDate?: Date;
+  xScale: ScaleTime<number, number>;
+}
+
+export default function SplitLinePopover({ paddingLeft, splitPointDate, xScale }: Readonly<Props>) {
+  const [popoverOpen, setPopoverOpen] = React.useState(false);
+  const showSplitLine = shouldShowSplitLine(splitPointDate, xScale);
+
+  if (!showSplitLine) {
+    return null;
+  }
+
+  return (
+    <Popover
+      isOpen={popoverOpen}
+      title={translate('project_activity.graphs.rating_split.title')}
+      description={translate('project_activity.graphs.rating_split.description')}
+      footer={
+        <DocumentationLink to={DocLink.MetricDefinitions}>
+          {translate('learn_more')}
+        </DocumentationLink>
+      }
+    >
+      <ButtonIcon
+        isIconFilled
+        style={{ left: `${Math.round(xScale(splitPointDate)) + paddingLeft}px` }}
+        className="sw-border-none sw-absolute sw-bg-transparent sw--top-3 sw--translate-x-2/4"
+        ariaLabel={translate('project_activity.graphs.rating_split.info_icon')}
+        Icon={IconInfo}
+        onClick={() => setPopoverOpen(!popoverOpen)}
+      />
+    </Popover>
+  );
+}
index 857ea92689477c602311d271dc3f703c2fe20d76..fbf0cad0c047298faf4ec94c5b13013608680413 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 { TooltipProvider } from '@sonarsource/echoes-react';
 import { render } from '@testing-library/react';
 import * as React from 'react';
 import { MetricType } from '~sonar-aligned/types/metrics';
@@ -60,45 +61,61 @@ it('should render correctly', () => {
   checkSnapShot({ zoomSpeed: 2 }, 'zoomSpeed');
   checkSnapShot({ leakPeriodDate: new Date('2019-10-02T00:00:00.000Z') }, 'leakPeriodDate');
   checkSnapShot({ basisCurve: true }, 'basisCurve');
+  checkSnapShot({ isLegacy: false }, 'not legacy');
+  checkSnapShot(
+    { isLegacy: false, splitPointDate: new Date('2019-10-02T00:00:00.000Z') },
+    'not legacy + split point, but not Rating',
+  );
+  checkSnapShot(
+    {
+      isLegacy: false,
+      splitPointDate: new Date('2019-10-02T00:00:00.000Z'),
+      metricType: MetricType.Rating,
+    },
+    'not legacy + split point',
+  );
 });
 
 function renderComponent(props?: Partial<PropsWithoutTheme>) {
   return render(
-    <AdvancedTimeline
-      height={100}
-      maxYTicksCount={10}
-      metricType="TEST_METRIC"
-      series={[
-        {
-          name: 'test-1',
-          type: 'test-type-1',
-          translatedName: '',
-          data: [
-            {
-              x: new Date('2019-10-01T00:00:00.000Z'),
-              y: 1,
-            },
-            {
-              x: new Date('2019-10-02T00:00:00.000Z'),
-              y: 2,
-            },
-          ],
-        },
-        {
-          name: 'test-2',
-          type: 'test-type-2',
-          translatedName: '',
-          data: [
-            {
-              x: new Date('2019-10-03T00:00:00.000Z'),
-              y: 3,
-            },
-          ],
-        },
-      ]}
-      width={100}
-      zoomSpeed={1}
-      {...props}
-    />,
+    <TooltipProvider>
+      <AdvancedTimeline
+        height={100}
+        maxYTicksCount={10}
+        metricType="TEST_METRIC"
+        series={[
+          {
+            name: 'test-1',
+            type: 'test-type-1',
+            translatedName: '',
+            data: [
+              {
+                x: new Date('2019-10-01T00:00:00.000Z'),
+                y: 1,
+              },
+              {
+                x: new Date('2019-10-02T00:00:00.000Z'),
+                y: 2,
+              },
+            ],
+          },
+          {
+            name: 'test-2',
+            type: 'test-type-2',
+            translatedName: '',
+            data: [
+              {
+                x: new Date('2019-10-03T00:00:00.000Z'),
+                y: 3,
+              },
+            ],
+          },
+        ]}
+        width={100}
+        zoomSpeed={1}
+        isLegacy
+        {...props}
+      />
+    </TooltipProvider>,
   );
 }
index 65b69b04b6cf29451f4cc8729cbdf6a9ab9e2ca8..1d357bc97b8f4772064aecf09ecd2d3723d374f7 100644 (file)
@@ -1913,6 +1913,698 @@ exports[`should render correctly: no height 1`] = `null`;
 
 exports[`should render correctly: no width 1`] = `null`;
 
+exports[`should render correctly: not legacy + split point 1`] = `
+<svg
+  class="line-chart"
+  height="100"
+  width="100"
+>
+  <g
+    transform="translate(50, 26)"
+  >
+    <g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="24"
+          y2="24"
+        />
+      </g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="16"
+          y2="16"
+        />
+      </g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="8"
+          y2="8"
+        />
+      </g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="0"
+          y2="0"
+        />
+      </g>
+    </g>
+    <g
+      transform="translate(0, 20)"
+    >
+      <text
+        class="line-chart-tick sw-body-sm"
+        text-anchor="end"
+        transform="rotate(-35, 15, 24)"
+        x="15"
+        y="24"
+      >
+        October
+      </text>
+      <text
+        class="line-chart-tick sw-body-sm"
+        text-anchor="end"
+        transform="rotate(-35, 20, 24)"
+        x="20"
+        y="24"
+      >
+        06 AM
+      </text>
+      <text
+        class="line-chart-tick sw-body-sm"
+        text-anchor="end"
+        transform="rotate(-35, 25, 24)"
+        x="25"
+        y="24"
+      >
+        12 PM
+      </text>
+      <text
+        class="line-chart-tick sw-body-sm"
+        text-anchor="end"
+        transform="rotate(-35, 30, 24)"
+        x="30"
+        y="24"
+      >
+        06 PM
+      </text>
+      <text
+        class="line-chart-tick sw-body-sm"
+        text-anchor="end"
+        transform="rotate(-35, 35, 24)"
+        x="35"
+        y="24"
+      >
+        Wed 02
+      </text>
+      <text
+        class="line-chart-tick sw-body-sm"
+        text-anchor="end"
+        transform="rotate(-35, 40, 24)"
+        x="40"
+        y="24"
+      >
+        06 AM
+      </text>
+      <text
+        class="line-chart-tick sw-body-sm"
+        text-anchor="end"
+        transform="rotate(-35, 45, 24)"
+        x="45"
+        y="24"
+      >
+        12 PM
+      </text>
+      <text
+        class="line-chart-tick sw-body-sm"
+        text-anchor="end"
+        transform="rotate(-35, 50, 24)"
+        x="50"
+        y="24"
+      >
+        06 PM
+      </text>
+    </g>
+    <g>
+      <path
+        class="line-chart-path line-chart-path-0"
+        d="M0,0L20,8"
+        stroke="rgb(85,170,223)"
+        stroke-dasharray="0"
+      />
+      <path
+        class="line-chart-path line-chart-path-1"
+        d="M40,16Z"
+        stroke="rgb(58,127,173)"
+        stroke-dasharray="3"
+      />
+    </g>
+    <g>
+      <circle
+        cx="40"
+        cy="16"
+        fill="rgb(58,127,173)"
+        r="2"
+        stroke="white"
+        stroke-width="1"
+      />
+    </g>
+    <rect
+      class="chart-mouse-events-overlay"
+      height="24"
+      width="40"
+    />
+    <line
+      class="line-tooltip"
+      stroke-dasharray="2"
+      x1="20"
+      x2="20"
+      y1="24"
+      y2="-10"
+    />
+  </g>
+</svg>
+`;
+
+exports[`should render correctly: not legacy + split point, but not Rating 1`] = `
+<svg
+  class="line-chart"
+  height="100"
+  width="100"
+>
+  <g
+    transform="translate(50, 26)"
+  >
+    <g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="24"
+          y2="24"
+        />
+      </g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="22.4"
+          y2="22.4"
+        />
+      </g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="20.8"
+          y2="20.8"
+        />
+      </g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="19.200000000000003"
+          y2="19.200000000000003"
+        />
+      </g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="17.6"
+          y2="17.6"
+        />
+      </g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="16"
+          y2="16"
+        />
+      </g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="14.400000000000002"
+          y2="14.400000000000002"
+        />
+      </g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="12.800000000000002"
+          y2="12.800000000000002"
+        />
+      </g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="11.2"
+          y2="11.2"
+        />
+      </g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="9.600000000000001"
+          y2="9.600000000000001"
+        />
+      </g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="8"
+          y2="8"
+        />
+      </g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="6.399999999999999"
+          y2="6.399999999999999"
+        />
+      </g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="4.800000000000002"
+          y2="4.800000000000002"
+        />
+      </g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="3.1999999999999993"
+          y2="3.1999999999999993"
+        />
+      </g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="1.6000000000000023"
+          y2="1.6000000000000023"
+        />
+      </g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="0"
+          y2="0"
+        />
+      </g>
+    </g>
+    <g
+      transform="translate(0, 20)"
+    >
+      <text
+        class="line-chart-tick sw-body-sm"
+        text-anchor="end"
+        transform="rotate(-35, 15, 24)"
+        x="15"
+        y="24"
+      >
+        October
+      </text>
+      <text
+        class="line-chart-tick sw-body-sm"
+        text-anchor="end"
+        transform="rotate(-35, 20, 24)"
+        x="20"
+        y="24"
+      >
+        06 AM
+      </text>
+      <text
+        class="line-chart-tick sw-body-sm"
+        text-anchor="end"
+        transform="rotate(-35, 25, 24)"
+        x="25"
+        y="24"
+      >
+        12 PM
+      </text>
+      <text
+        class="line-chart-tick sw-body-sm"
+        text-anchor="end"
+        transform="rotate(-35, 30, 24)"
+        x="30"
+        y="24"
+      >
+        06 PM
+      </text>
+      <text
+        class="line-chart-tick sw-body-sm"
+        text-anchor="end"
+        transform="rotate(-35, 35, 24)"
+        x="35"
+        y="24"
+      >
+        Wed 02
+      </text>
+      <text
+        class="line-chart-tick sw-body-sm"
+        text-anchor="end"
+        transform="rotate(-35, 40, 24)"
+        x="40"
+        y="24"
+      >
+        06 AM
+      </text>
+      <text
+        class="line-chart-tick sw-body-sm"
+        text-anchor="end"
+        transform="rotate(-35, 45, 24)"
+        x="45"
+        y="24"
+      >
+        12 PM
+      </text>
+      <text
+        class="line-chart-tick sw-body-sm"
+        text-anchor="end"
+        transform="rotate(-35, 50, 24)"
+        x="50"
+        y="24"
+      >
+        06 PM
+      </text>
+    </g>
+    <g>
+      <path
+        class="line-chart-path line-chart-path-0"
+        d="M0,16L20,8"
+        stroke="rgb(85,170,223)"
+        stroke-dasharray="0"
+      />
+      <path
+        class="line-chart-path line-chart-path-1"
+        d="M40,0Z"
+        stroke="rgb(58,127,173)"
+        stroke-dasharray="3"
+      />
+    </g>
+    <g>
+      <circle
+        cx="40"
+        cy="0"
+        fill="rgb(58,127,173)"
+        r="2"
+        stroke="white"
+        stroke-width="1"
+      />
+    </g>
+    <rect
+      class="chart-mouse-events-overlay"
+      height="24"
+      width="40"
+    />
+  </g>
+</svg>
+`;
+
+exports[`should render correctly: not legacy 1`] = `
+<svg
+  class="line-chart"
+  height="100"
+  width="100"
+>
+  <g
+    transform="translate(50, 26)"
+  >
+    <g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="24"
+          y2="24"
+        />
+      </g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="22.4"
+          y2="22.4"
+        />
+      </g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="20.8"
+          y2="20.8"
+        />
+      </g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="19.200000000000003"
+          y2="19.200000000000003"
+        />
+      </g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="17.6"
+          y2="17.6"
+        />
+      </g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="16"
+          y2="16"
+        />
+      </g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="14.400000000000002"
+          y2="14.400000000000002"
+        />
+      </g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="12.800000000000002"
+          y2="12.800000000000002"
+        />
+      </g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="11.2"
+          y2="11.2"
+        />
+      </g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="9.600000000000001"
+          y2="9.600000000000001"
+        />
+      </g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="8"
+          y2="8"
+        />
+      </g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="6.399999999999999"
+          y2="6.399999999999999"
+        />
+      </g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="4.800000000000002"
+          y2="4.800000000000002"
+        />
+      </g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="3.1999999999999993"
+          y2="3.1999999999999993"
+        />
+      </g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="1.6000000000000023"
+          y2="1.6000000000000023"
+        />
+      </g>
+      <g>
+        <line
+          class="line-chart-grid"
+          x1="0"
+          x2="40"
+          y1="0"
+          y2="0"
+        />
+      </g>
+    </g>
+    <g
+      transform="translate(0, 20)"
+    >
+      <text
+        class="line-chart-tick sw-body-sm"
+        text-anchor="end"
+        transform="rotate(-35, 15, 24)"
+        x="15"
+        y="24"
+      >
+        October
+      </text>
+      <text
+        class="line-chart-tick sw-body-sm"
+        text-anchor="end"
+        transform="rotate(-35, 20, 24)"
+        x="20"
+        y="24"
+      >
+        06 AM
+      </text>
+      <text
+        class="line-chart-tick sw-body-sm"
+        text-anchor="end"
+        transform="rotate(-35, 25, 24)"
+        x="25"
+        y="24"
+      >
+        12 PM
+      </text>
+      <text
+        class="line-chart-tick sw-body-sm"
+        text-anchor="end"
+        transform="rotate(-35, 30, 24)"
+        x="30"
+        y="24"
+      >
+        06 PM
+      </text>
+      <text
+        class="line-chart-tick sw-body-sm"
+        text-anchor="end"
+        transform="rotate(-35, 35, 24)"
+        x="35"
+        y="24"
+      >
+        Wed 02
+      </text>
+      <text
+        class="line-chart-tick sw-body-sm"
+        text-anchor="end"
+        transform="rotate(-35, 40, 24)"
+        x="40"
+        y="24"
+      >
+        06 AM
+      </text>
+      <text
+        class="line-chart-tick sw-body-sm"
+        text-anchor="end"
+        transform="rotate(-35, 45, 24)"
+        x="45"
+        y="24"
+      >
+        12 PM
+      </text>
+      <text
+        class="line-chart-tick sw-body-sm"
+        text-anchor="end"
+        transform="rotate(-35, 50, 24)"
+        x="50"
+        y="24"
+      >
+        06 PM
+      </text>
+    </g>
+    <g>
+      <path
+        class="line-chart-path line-chart-path-0"
+        d="M0,16L20,8"
+        stroke="rgb(85,170,223)"
+        stroke-dasharray="0"
+      />
+      <path
+        class="line-chart-path line-chart-path-1"
+        d="M40,0Z"
+        stroke="rgb(58,127,173)"
+        stroke-dasharray="3"
+      />
+    </g>
+    <g>
+      <circle
+        cx="40"
+        cy="0"
+        fill="rgb(58,127,173)"
+        r="2"
+        stroke="white"
+        stroke-width="1"
+      />
+    </g>
+    <rect
+      class="chart-mouse-events-overlay"
+      height="24"
+      width="40"
+    />
+  </g>
+</svg>
+`;
+
 exports[`should render correctly: rating metric 1`] = `
 <svg
   class="line-chart"
diff --git a/server/sonar-web/src/main/js/helpers/activity-graph.ts b/server/sonar-web/src/main/js/helpers/activity-graph.ts
new file mode 100644 (file)
index 0000000..d9472f6
--- /dev/null
@@ -0,0 +1,102 @@
+/*
+ * 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 { ScaleTime } from 'd3-scale';
+import { TimeMachineResponse } from '../api/time-machine';
+import { SOFTWARE_QUALITY_RATING_METRICS_MAP } from './constants';
+
+export const mergeRatingMeasureHistory = (
+  historyData: TimeMachineResponse | undefined,
+  parseDateFn: (date: string) => Date,
+  isLegacy: boolean = false,
+) => {
+  const softwareQualityMeasures = Object.values(SOFTWARE_QUALITY_RATING_METRICS_MAP);
+  const softwareQualityMeasuresMap = new Map<
+    string,
+    { history: { date: string; value?: string }[]; index: number; splitDate?: Date }
+  >();
+  if (isLegacy) {
+    return (
+      historyData?.measures
+        ?.filter((m) => !softwareQualityMeasures.includes(m.metric))
+        .map((measure) => ({
+          metric: measure.metric,
+          history: measure.history.map((historyItem) => ({
+            date: parseDateFn(historyItem.date),
+            value: historyItem.value,
+          })),
+        })) ?? []
+    );
+  }
+
+  const historyDataFiltered =
+    historyData?.measures?.filter((measure) => {
+      if (softwareQualityMeasures.includes(measure.metric)) {
+        const splitPointIndex = measure.history.findIndex(
+          (historyItem) => historyItem.value != null,
+        );
+        softwareQualityMeasuresMap.set(measure.metric, {
+          history: measure.history,
+          index: measure.history.findIndex((historyItem) => historyItem.value != null),
+          splitDate:
+            // Don't show splitPoint if it's the first history item
+            splitPointIndex !== -1 && splitPointIndex !== 0
+              ? parseDateFn(measure.history[splitPointIndex].date)
+              : undefined,
+        });
+        return false;
+      }
+      return true;
+    }) ?? [];
+
+  const historyMapper = (historyItem: { date: string; value?: string }) => ({
+    date: parseDateFn(historyItem.date),
+    value:
+      softwareQualityMeasuresMap.size > 0 && historyItem.value === '5.0'
+        ? '4.0'
+        : historyItem.value,
+  });
+
+  return historyDataFiltered.map((measure) => {
+    const softwareQualityMetric = softwareQualityMeasuresMap.get(
+      SOFTWARE_QUALITY_RATING_METRICS_MAP[measure.metric],
+    );
+    return {
+      metric: measure.metric,
+      splitPointDate: softwareQualityMetric ? softwareQualityMetric.splitDate : undefined,
+      history: softwareQualityMetric
+        ? measure.history
+            .slice(0, softwareQualityMetric.index)
+            .map(historyMapper)
+            .concat(
+              softwareQualityMetric.history.slice(softwareQualityMetric.index).map(historyMapper),
+            )
+        : measure.history.map(historyMapper),
+    };
+  });
+};
+
+export const shouldShowSplitLine = (
+  splitPointDate: Date | undefined,
+  xScale: ScaleTime<number, number>,
+): splitPointDate is Date =>
+  splitPointDate !== undefined &&
+  xScale(splitPointDate) >= xScale.range()[0] &&
+  xScale(splitPointDate) <= xScale.range()[1];
index 4bb0a7f82b6e1c87c308c1073c478ac7cab189c7..44b98806ac6c176fc47554dfad53cb6d44ce42e1 100644 (file)
@@ -618,6 +618,56 @@ export const DEFAULT_METRICS: Dict<Metric> = {
     qualitative: false,
     hidden: true,
   },
+  last_change_on_software_quality_maintainability_rating: {
+    id: 'f82cff1f-a70a-497a-bca8-33d8abb20e2e',
+    key: 'last_change_on_software_quality_maintainability_rating',
+    type: 'DATA',
+    name: 'Last Change on Software Quality Maintainability Rating',
+    domain: 'Maintainability',
+    direction: 0,
+    qualitative: false,
+    hidden: true,
+  },
+  last_change_on_software_quality_releasability_rating: {
+    id: 'a2aebcc4-366d-49b3-852b-c7b36f43e1c7',
+    key: 'last_change_on_software_quality_releasability_rating',
+    type: 'DATA',
+    name: 'Last Change on Software Quality Releasability Rating',
+    domain: 'Releasability',
+    direction: 0,
+    qualitative: false,
+    hidden: true,
+  },
+  last_change_on_software_quality_reliability_rating: {
+    id: '42889539-14b7-45a5-a383-8c4d4a5e48a5',
+    key: 'last_change_on_software_quality_reliability_rating',
+    type: 'DATA',
+    name: 'Last Change on Software Quality Reliability Rating',
+    domain: 'Reliability',
+    direction: 0,
+    qualitative: false,
+    hidden: true,
+  },
+  last_change_on_software_quality_security_rating: {
+    id: 'd236e941-90e8-4c35-b995-47d05637b6a4',
+    key: 'last_change_on_software_quality_security_rating',
+    type: 'DATA',
+    name: 'Last Change on Software Quality Security Rating',
+    domain: 'Security',
+    direction: 0,
+    qualitative: false,
+    hidden: true,
+  },
+  last_change_on_software_quality_security_review_rating: {
+    id: '0f4f143d-b76b-40c9-8769-7f959f4a49ea',
+    key: 'last_change_on_software_quality_security_review_rating',
+    type: 'DATA',
+    name: 'Last Change on SoftwareQuality Security Review Rating',
+    domain: 'Security',
+    direction: 0,
+    qualitative: false,
+    hidden: true,
+  },
   line_coverage: {
     id: 'AXJMbIl_PAOIsUIE3gtl',
     key: 'line_coverage',
@@ -1126,7 +1176,7 @@ export const DEFAULT_METRICS: Dict<Metric> = {
     key: 'reliability_rating_distribution',
     type: 'DATA',
     name: 'Reliability Rating Distribution',
-    description: 'Maintainability rating distribution',
+    description: 'Reliability rating distribution',
     domain: 'Reliability',
     direction: -1,
     qualitative: true,
@@ -1137,7 +1187,7 @@ export const DEFAULT_METRICS: Dict<Metric> = {
     key: 'new_reliability_rating_distribution',
     type: 'DATA',
     name: 'Reliability Rating Distribution on New Code',
-    description: 'Maintainability rating distribution on new code',
+    description: 'Reliability rating distribution on new code',
     domain: 'Reliability',
     direction: -1,
     qualitative: true,
@@ -1420,6 +1470,38 @@ export const DEFAULT_METRICS: Dict<Metric> = {
     qualitative: true,
     hidden: false,
   },
+  software_quality_maintainability_rating_distribution: {
+    id: 'b39b797b-216d-4800-810e-2277012ee096',
+    key: 'software_quality_maintainability_rating_distribution',
+    type: 'DATA',
+    name: 'Software Quality Maintainability Rating Distribution',
+    description: 'Software Quality Maintainability rating distribution',
+    domain: 'Maintainability',
+    direction: -1,
+    qualitative: true,
+    hidden: true,
+  },
+  new_software_quality_maintainability_rating_distribution: {
+    id: '21d0f133-de6d-4b2e-8302-99169720f8c6',
+    key: 'new_software_quality_maintainability_rating_distribution',
+    type: 'DATA',
+    name: 'Software Quality Maintainability Rating Distribution on New Code',
+    description: 'Software Quality Maintainability rating distribution on new code',
+    domain: 'Maintainability',
+    direction: -1,
+    qualitative: true,
+    hidden: true,
+  },
+  software_quality_maintainability_rating_effort: {
+    id: '0a25e15c-10c9-4d66-8dc0-41446319405d',
+    key: 'software_quality_maintainability_rating_effort',
+    type: 'DATA',
+    name: 'Software Quality Maintainability Rating Effort',
+    domain: 'Maintainability',
+    direction: 0,
+    qualitative: false,
+    hidden: true,
+  },
   new_software_quality_maintainability_rating: {
     id: 'c5d12cc4-e712-4701-a395-c9113ce13c3e',
     key: 'new_software_quality_maintainability_rating',
@@ -1455,6 +1537,27 @@ export const DEFAULT_METRICS: Dict<Metric> = {
     qualitative: true,
     hidden: false,
   },
+  software_quality_releasability_rating: {
+    id: '1fb38855-84b8-41b2-88a0-50c3dceda102',
+    key: 'software_quality_releasability_rating',
+    type: 'RATING',
+    name: 'Software Quality Releasability rating',
+    domain: 'Releasability',
+    direction: -1,
+    qualitative: true,
+    hidden: false,
+  },
+  software_quality_releasability_rating_distribution: {
+    id: 'a34a08a2-29b5-4efb-bd2a-eebe3dc10dab',
+    key: 'software_quality_releasability_rating_distribution',
+    type: 'DATA',
+    name: 'Software Quality Releasability Rating Distribution',
+    description: 'Software Quality Releasability rating distribution',
+    domain: 'Releasability',
+    direction: -1,
+    qualitative: true,
+    hidden: true,
+  },
   software_quality_reliability_rating: {
     id: '6548ffa4-8a5e-4445-a28d-e2fd9fdbba78',
     key: 'software_quality_reliability_rating',
@@ -1466,6 +1569,38 @@ export const DEFAULT_METRICS: Dict<Metric> = {
     qualitative: true,
     hidden: false,
   },
+  software_quality_reliability_rating_distribution: {
+    id: '571de2d7-d1ef-460b-8f99-e29e0aa6218c',
+    key: 'software_quality_reliability_rating_distribution',
+    type: 'DATA',
+    name: 'Software Quality Reliability Rating Distribution',
+    description: 'Software Quality Reliability rating distribution',
+    domain: 'Reliability',
+    direction: -1,
+    qualitative: true,
+    hidden: true,
+  },
+  new_software_quality_reliability_rating_distribution: {
+    id: '77693e0a-fc61-465f-8fe0-a5fe77f4d507',
+    key: 'new_software_quality_reliability_rating_distribution',
+    type: 'DATA',
+    name: 'Software Quality Reliability Rating Distribution on New Code',
+    description: 'Software Quality Reliability rating distribution on new code',
+    domain: 'Reliability',
+    direction: -1,
+    qualitative: true,
+    hidden: true,
+  },
+  software_quality_reliability_rating_effort: {
+    id: '38f61088-42f3-437f-b2b5-65a8fa4c7448',
+    key: 'software_quality_reliability_rating_effort',
+    type: 'DATA',
+    name: 'Software Quality Reliability Rating Effort',
+    domain: 'Reliability',
+    direction: 0,
+    qualitative: false,
+    hidden: true,
+  },
   new_software_quality_reliability_rating: {
     id: 'ab82dcac-cf81-4780-965d-1384ce9e8983',
     key: 'new_software_quality_reliability_rating',
@@ -1510,6 +1645,38 @@ export const DEFAULT_METRICS: Dict<Metric> = {
     qualitative: true,
     hidden: false,
   },
+  software_quality_security_rating_distribution: {
+    id: 'f9a76abe-7663-47b3-a27c-1dea7e6b4861',
+    key: 'software_quality_security_rating_distribution',
+    type: 'DATA',
+    name: 'Software Quality Security Rating Distribution',
+    description: 'Software Quality Security rating distribution',
+    domain: 'Security',
+    direction: -1,
+    qualitative: true,
+    hidden: true,
+  },
+  new_software_quality_security_rating_distribution: {
+    id: '2f1155cb-3802-463a-95d3-dd352bafdc0c',
+    key: 'new_software_quality_security_rating_distribution',
+    type: 'DATA',
+    name: 'Software Quality Security Rating Distribution on New Code',
+    description: 'Software Quality Security rating distribution on new code',
+    domain: 'Security',
+    direction: -1,
+    qualitative: true,
+    hidden: true,
+  },
+  software_quality_security_rating_effort: {
+    id: 'be673f93-1b72-418c-b134-b097dae65048',
+    key: 'software_quality_security_rating_effort',
+    type: 'DATA',
+    name: 'Software Quality Security Rating Effort',
+    domain: 'Security',
+    direction: 0,
+    qualitative: false,
+    hidden: true,
+  },
   new_software_quality_security_rating: {
     id: '228b9a04-09a2-418e-9ea4-3584a57a95ba',
     key: 'new_software_quality_security_rating',
@@ -1554,6 +1721,38 @@ export const DEFAULT_METRICS: Dict<Metric> = {
     qualitative: true,
     hidden: false,
   },
+  software_quality_security_review_rating_distribution: {
+    id: '3b9d046c-a319-4cbf-99c4-15c206e71401',
+    key: 'software_quality_security_review_rating_distribution',
+    type: 'DATA',
+    name: 'software Quality Security Review Rating Distribution',
+    description: 'Software Quality Security review rating distribution',
+    domain: 'Security',
+    direction: -1,
+    qualitative: true,
+    hidden: true,
+  },
+  new_software_quality_security_review_rating_distribution: {
+    id: 'b6565b9b-3676-405f-8149-a37cb0bd78b9',
+    key: 'new_software_quality_security_review_rating_distribution',
+    type: 'DATA',
+    name: 'Software Quality Security Review Rating Distribution on New Code',
+    description: 'Software Quality Security review rating distribution on new code',
+    domain: 'Security',
+    direction: -1,
+    qualitative: true,
+    hidden: true,
+  },
+  software_quality_security_review_rating_effort: {
+    id: '8d6d023e-6764-42ee-9fb7-bc1f17bda205',
+    key: 'software_quality_security_review_rating_effort',
+    type: 'DATA',
+    name: 'Software Quality Security Review Rating Effort',
+    domain: 'Security',
+    direction: 0,
+    qualitative: false,
+    hidden: true,
+  },
   new_software_quality_security_review_rating: {
     id: '4d4b1d18-da7e-403c-a3f0-99951c58b050',
     key: 'new_software_quality_security_review_rating',
index 0034d72d883eac85e672f44dda9bf0a3bea42f24..642564b73d47e15830054edc2f602418d4668b25 100644 (file)
@@ -22,22 +22,9 @@ import { groupBy, omit } from 'lodash';
 import { BranchParameters } from '~sonar-aligned/types/branch-like';
 import { getTasksForComponent } from '../api/ce';
 import { getBreadcrumbs, getComponent, getComponentData } from '../api/components';
-import { MetricKey } from '../sonar-aligned/types/metrics';
 import { Component, Measure } from '../types/types';
 import { StaleTime, createQueryHook } from './common';
 
-const NEW_METRICS = [
-  MetricKey.software_quality_maintainability_rating,
-  MetricKey.software_quality_security_rating,
-  MetricKey.software_quality_reliability_rating,
-  MetricKey.software_quality_security_review_rating,
-  MetricKey.software_quality_releasability_rating,
-  MetricKey.new_software_quality_security_rating,
-  MetricKey.new_software_quality_reliability_rating,
-  MetricKey.new_software_quality_maintainability_rating,
-  MetricKey.new_software_quality_security_review_rating,
-];
-
 const TASK_RETRY = 10_000;
 
 type QueryKeyData = {
@@ -70,10 +57,7 @@ export const useComponentQuery = createQueryHook(
       queryFn: async () => {
         const result = await getComponent({
           component,
-          metricKeys: metricKeys
-            .split(',')
-            .filter((m) => !NEW_METRICS.includes(m as MetricKey))
-            .join(),
+          metricKeys,
           ...params,
         });
         const measuresMapByMetricKey = groupBy(result.component.measures, 'metric');
index 5b5194bf1e04717cca1ff775c6251eaeb1e2ec1b..38ebd2c0c82b2574c76a8f4fe897c3e2e03cb59b 100644 (file)
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
 
-import {
-  infiniteQueryOptions,
-  queryOptions,
-  useQuery,
-  useQueryClient,
-} from '@tanstack/react-query';
+import { infiniteQueryOptions, queryOptions, useQueryClient } from '@tanstack/react-query';
 import { groupBy, isUndefined, omitBy } from 'lodash';
 import { BranchParameters } from '~sonar-aligned/types/branch-like';
 import { getComponentTree } from '../api/components';
@@ -40,26 +35,28 @@ import { BranchLike } from '../types/branch-like';
 import { Measure } from '../types/types';
 import { createInfiniteQueryHook, createQueryHook, StaleTime } from './common';
 
-export function useAllMeasuresHistoryQuery(
-  component: string | undefined,
-  branchParams: BranchParameters,
-  metrics: string,
-  enabled = true,
-) {
-  return useQuery({
-    queryKey: ['measures', 'history', component, branchParams, metrics],
-    queryFn: () => {
-      if (metrics.length <= 0) {
-        return Promise.resolve({
-          measures: [],
-          paging: { pageIndex: 1, pageSize: 1, total: 0 },
-        });
-      }
-      return getAllTimeMachineData({ component, metrics, ...branchParams, p: 1 });
-    },
-    enabled,
-  });
-}
+export const useAllMeasuresHistoryQuery = createQueryHook(
+  ({
+    component,
+    branchParams,
+    metrics,
+  }: Omit<Parameters<typeof getAllTimeMachineData>[0], 'to' | 'from' | 'p'> & {
+    branchParams?: BranchParameters;
+  }) => {
+    return queryOptions({
+      queryKey: ['measures', 'history', component, branchParams, metrics],
+      queryFn: () => {
+        if (metrics.length <= 0) {
+          return Promise.resolve({
+            measures: [],
+            paging: { pageIndex: 1, pageSize: 1, total: 0 },
+          });
+        }
+        return getAllTimeMachineData({ component, metrics, ...branchParams, p: 1 });
+      },
+    });
+  },
+);
 
 export const useMeasuresComponentQuery = createQueryHook(
   ({
@@ -79,9 +76,7 @@ export const useMeasuresComponentQuery = createQueryHook(
       queryFn: async () => {
         const data = await getMeasuresWithPeriodAndMetrics(
           componentKey,
-          metricKeys.filter(
-            (m) => ![MetricKey.software_quality_releasability_rating].includes(m as MetricKey),
-          ),
+          metricKeys,
           branchLikeQuery,
         );
         metricKeys.forEach((metricKey) => {
@@ -123,14 +118,11 @@ export const useComponentTreeQuery = createInfiniteQueryHook(
     return infiniteQueryOptions({
       queryKey: ['component', component, 'tree', strategy, { metrics, additionalData }],
       queryFn: async ({ pageParam }) => {
-        const result = await getComponentTree(
-          strategy,
-          component,
-          metrics?.filter(
-            (m) => ![MetricKey.software_quality_releasability_rating].includes(m as MetricKey),
-          ),
-          { ...additionalData, p: pageParam, ...branchLikeQuery },
-        );
+        const result = await getComponentTree(strategy, component, metrics, {
+          ...additionalData,
+          p: pageParam,
+          ...branchLikeQuery,
+        });
 
         if (result.baseComponent.measures && result.baseComponent.measures.length > 0) {
           const measuresMapByMetricKeyForBaseComponent = groupBy(
@@ -270,7 +262,6 @@ export const useMeasureQuery = createQueryHook(
 );
 
 const PORTFOLIO_OVERVIEW_METRIC_KEYS = [
-  MetricKey.software_quality_releasability_rating,
   MetricKey.software_quality_releasability_rating_distribution,
   MetricKey.software_quality_security_rating_distribution,
   MetricKey.software_quality_security_review_rating_distribution,
index 16611694225efd61d6bc4bfd723191c792288d4e..cedb36929eafa1bb8a61c282a82e93cb80443013 100644 (file)
@@ -99,6 +99,7 @@ export interface HistoryItem {
 export interface MeasureHistory {
   history: HistoryItem[];
   metric: MetricKey;
+  splitPointDate?: Date;
 }
 
 export interface Serie {
index 41ac785abeb29e4b3af2dd8c41ed715731b10c48..2389ea590ce53bd79387e5c2bef8108b5ddec8ee 100644 (file)
@@ -2033,6 +2033,10 @@ project_activity.graphs.data_table.no_data_warning_check_dates_y=There is no dat
 project_activity.graphs.data_table.no_data_warning_check_dates_x_y=There is no data for the selected date range ({start} to {end}). Try modifying the date filters on the main page.
 project_activity.graphs.data_table.data_gap=The chart history for issues related to software qualities may contain gaps while information is not available for one or more projects. {learn_more}
 
+project_activity.graphs.rating_split.title=Metrics calculation changed
+project_activity.graphs.rating_split.description=The way we calculate ratings has changed and it might have affected your ratings.
+project_activity.graphs.rating_split.info_icon=Metrics calculation change information
+
 project_activity.custom_metric.covered_lines=Covered Lines
 
 project_activity.custom_metric.deprecated.severity=Old severities and the corresponding filters are deprecated.