]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-21797 Add info message about chart gap for app/portfolio
authorstanislavh <stanislav.honcharov@sonarsource.com>
Mon, 25 Mar 2024 08:27:35 +0000 (09:27 +0100)
committersonartech <sonartech@sonarsource.com>
Mon, 25 Mar 2024 20:02:42 +0000 (20:02 +0000)
server/sonar-web/src/main/js/apps/projectActivity/components/ProjectActivityGraphs.tsx
server/sonar-web/src/main/js/apps/projectActivity/components/__tests__/ProjectActivityApp-it.tsx
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index 5557dbb24016159af046e89719f9c9ad19d3cb8b..a286a1ba5937500a03fbf63cabe8bda9a60b6aa7 100644 (file)
  * along with this program; if not, write to the Free Software Foundation,
  * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
  */
+import { FlagMessage } from 'design-system';
 import { debounce, findLast, maxBy, minBy, sortBy } from 'lodash';
 import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
 import GraphsHeader from '../../../components/activity-graph/GraphsHeader';
 import GraphsHistory from '../../../components/activity-graph/GraphsHistory';
 import GraphsZoom from '../../../components/activity-graph/GraphsZoom';
@@ -31,6 +33,9 @@ import {
   saveActivityGraph,
   splitSeriesInGraphs,
 } from '../../../components/activity-graph/utils';
+import DocumentationLink from '../../../components/common/DocumentationLink';
+import { CCT_SOFTWARE_QUALITY_METRICS } from '../../../helpers/constants';
+import { translate } from '../../../helpers/l10n';
 import {
   GraphType,
   MeasureHistory,
@@ -198,6 +203,48 @@ export default class ProjectActivityGraphs extends React.PureComponent<Props, St
     }
   };
 
+  renderQualitiesMetricInfoMessage = () => {
+    const { measuresHistory } = this.props;
+
+    const qualityMeasuresHistory = measuresHistory.find((history) =>
+      CCT_SOFTWARE_QUALITY_METRICS.includes(history.metric),
+    );
+
+    const indexOfFirstMeasureWithValue = qualityMeasuresHistory?.history.findIndex(
+      (item) => item.value,
+    );
+
+    const hasGaps =
+      indexOfFirstMeasureWithValue === -1
+        ? false
+        : qualityMeasuresHistory?.history
+            .slice(indexOfFirstMeasureWithValue)
+            .some((item) => item.value === undefined);
+
+    if (hasGaps) {
+      return (
+        <FlagMessage variant="info">
+          <FormattedMessage
+            id="project_activity.graphs.data_table.data_gap"
+            tagName="div"
+            values={{
+              learn_more: (
+                <DocumentationLink
+                  className="sw-whitespace-nowrap"
+                  to="/user-guide/clean-code/code-analysis/"
+                >
+                  {translate('learn_more')}
+                </DocumentationLink>
+              ),
+            }}
+          />
+        </FlagMessage>
+      );
+    }
+
+    return null;
+  };
+
   render() {
     const { analyses, leakPeriodDate, loading, measuresHistory, metrics, query } = this.props;
     const { graphEndDate, graphStartDate, series } = this.state;
@@ -214,6 +261,7 @@ export default class ProjectActivityGraphs extends React.PureComponent<Props, St
           selectedMetrics={query.customMetrics}
           onUpdateGraph={this.handleUpdateGraph}
         />
+        {this.renderQualitiesMetricInfoMessage()}
         <GraphsHistory
           analyses={analyses}
           graph={query.graph}
index 8666e5ef0bf2070aea053bf5e9d549ede86f98d7..d34221f2e412044ddaf8d916edd31675b079d16e 100644 (file)
@@ -86,12 +86,16 @@ beforeEach(() => {
       MetricKey.sqale_rating,
       MetricKey.security_hotspots_reviewed,
       MetricKey.security_review_rating,
+      MetricKey.maintainability_issues,
     ].map((metric) =>
       mockMeasureHistory({
         metric,
-        history: projectActivityHandler
-          .getAnalysesList()
-          .map(({ date }) => mockHistoryItem({ value: '3', date: parseDate(date) })),
+        history: projectActivityHandler.getAnalysesList().map(({ date }) =>
+          mockHistoryItem({
+            value: '3',
+            date: parseDate(date),
+          }),
+        ),
       }),
     ),
   );
@@ -241,6 +245,50 @@ describe('rendering', () => {
       ).not.toBeInTheDocument();
     },
   );
+
+  it('should render graph gap info message', async () => {
+    timeMachineHandler.setMeasureHistory([
+      mockMeasureHistory({
+        metric: MetricKey.maintainability_issues,
+        history: projectActivityHandler.getAnalysesList().map(({ date }, index) =>
+          mockHistoryItem({
+            // eslint-disable-next-line jest/no-conditional-in-test
+            value: index === 0 ? '3' : undefined,
+            date: parseDate(date),
+          }),
+        ),
+      }),
+    ]);
+    const { ui } = getPageObject();
+    renderProjectActivityAppContainer(
+      mockComponent({
+        breadcrumbs: [
+          { key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Application },
+        ],
+      }),
+    );
+
+    await ui.changeGraphType(GraphType.custom);
+    await ui.openMetricsDropdown();
+    await ui.toggleMetric(MetricKey.maintainability_issues);
+    expect(ui.gapInfoMessage.get()).toBeInTheDocument();
+  });
+
+  it('should not render graph gap info message if no gaps', async () => {
+    const { ui } = getPageObject();
+    renderProjectActivityAppContainer(
+      mockComponent({
+        breadcrumbs: [
+          { key: 'breadcrumb', name: 'breadcrumb', qualifier: ComponentQualifier.Application },
+        ],
+      }),
+    );
+
+    await ui.changeGraphType(GraphType.custom);
+    await ui.openMetricsDropdown();
+    await ui.toggleMetric(MetricKey.maintainability_issues);
+    expect(ui.gapInfoMessage.query()).not.toBeInTheDocument();
+  });
 });
 
 describe('CRUD', () => {
@@ -512,6 +560,7 @@ function getPageObject() {
     // Graphs.
     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 }),
 
     // Add metrics.
     addMetricBtn: byRole('button', { name: 'project_activity.graphs.custom.add' }),
@@ -697,6 +746,7 @@ function renderProjectActivityAppContainer(
     {
       metrics: keyBy(
         [
+          mockMetric({ key: MetricKey.maintainability_issues, type: MetricType.Data }),
           mockMetric({ key: MetricKey.bugs, type: MetricType.Integer }),
           mockMetric({ key: MetricKey.code_smells, type: MetricType.Integer }),
           mockMetric({ key: MetricKey.security_hotspots_reviewed }),
index b58a72b659b8a1871e6609d964dc47568c4afb39..ae13b51351aad503cc806d2e454d64e7176c80ee 100644 (file)
@@ -1950,6 +1950,7 @@ project_activity.graphs.data_table.no_data_warning=There is no data for the sele
 project_activity.graphs.data_table.no_data_warning_check_dates_x=There is no data for the selected date range (everything after {start}). Try modifying the date filters on the main page.
 project_activity.graphs.data_table.no_data_warning_check_dates_y=There is no data for the selected date range (everything before {end}). Try modifying the date filters on the main page.
 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.custom_metric.covered_lines=Covered Lines