]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-17011 [894182] Detailed alternative description is missing
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Tue, 11 Oct 2022 07:26:51 +0000 (09:26 +0200)
committersonartech <sonartech@sonarsource.com>
Thu, 13 Oct 2022 20:03:19 +0000 (20:03 +0000)
server/sonar-web/src/main/js/apps/overview/branches/ActivityPanel.tsx
server/sonar-web/src/main/js/apps/overview/branches/__tests__/__snapshots__/ActivityPanel-test.tsx.snap
server/sonar-web/src/main/js/components/activity-graph/DataTableModal.tsx [new file with mode: 0644]
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/activity-graph/GraphsZoom.tsx
server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphHistory-test.tsx.snap
server/sonar-web/src/main/js/components/activity-graph/__tests__/__snapshots__/GraphsHistory-test.tsx.snap
server/sonar-web/src/main/js/components/activity-graph/utils.ts
sonar-core/src/main/resources/org/sonar/l10n/core.properties

index b737b1bbd079fdaf63825d426c3d0e911643cced..c894d53d81976d2a31fb3482f4ed3cda9afc95d0 100644 (file)
@@ -98,6 +98,7 @@ export function ActivityPanel(props: ActivityPanelProps) {
                   'overview.activity.graph_shows_data_for_x',
                   displayedMetrics.map(metricKey => localizeMetric(metricKey)).join(', ')
                 )}
+                canShowDataAsTable={false}
                 graph={graph}
                 graphs={graphs}
                 leakPeriodDate={shownLeakPeriodDate}
index ca28c6eba5e50e4205de696edc9802c441d81859..1a33e48545e95a875e979734bf2c013e9be2da55 100644 (file)
@@ -39,6 +39,7 @@ exports[`should render correctly 1`] = `
           <GraphsHistory
             analyses={Array []}
             ariaLabel="overview.activity.graph_shows_data_for_x.metric.bugs.name, metric.code_smells.name, metric.vulnerabilities.name"
+            canShowDataAsTable={false}
             graph="issues"
             graphs={
               Array [
@@ -208,6 +209,7 @@ exports[`should render correctly 2`] = `
           <GraphsHistory
             analyses={Array []}
             ariaLabel="overview.activity.graph_shows_data_for_x.metric.bugs.name, metric.code_smells.name, metric.vulnerabilities.name"
+            canShowDataAsTable={false}
             graph="issues"
             graphs={
               Array [
diff --git a/server/sonar-web/src/main/js/components/activity-graph/DataTableModal.tsx b/server/sonar-web/src/main/js/components/activity-graph/DataTableModal.tsx
new file mode 100644 (file)
index 0000000..8cfea76
--- /dev/null
@@ -0,0 +1,188 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2022 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 { filter, slice, sortBy } from 'lodash';
+import * as React from 'react';
+import { FormattedMessage } from 'react-intl';
+import EventInner from '../../apps/projectActivity/components/EventInner';
+import { translate, translateWithParameters } from '../../helpers/l10n';
+import { formatMeasure } from '../../helpers/measures';
+import { ParsedAnalysis, Serie } from '../../types/project-activity';
+import { Button } from '../controls/buttons';
+import Modal from '../controls/Modal';
+import DateFormatter from '../intl/DateFormatter';
+import TimeFormatter from '../intl/TimeFormatter';
+import { Alert } from '../ui/Alert';
+import { getAnalysisEventsForDate } from './utils';
+
+export interface DataTableModalProps {
+  analyses: ParsedAnalysis[];
+  graphEndDate?: Date;
+  graphStartDate?: Date;
+  series: Serie[];
+  onClose: () => void;
+}
+
+type DataTableEntry = { date: Date } & { [x: string]: string | undefined };
+
+const MAX_DATA_TABLE_ROWS = 100;
+
+export default function DataTableModal(props: DataTableModalProps) {
+  const { analyses, series, graphEndDate, graphStartDate } = props;
+
+  if (series.length === 0) {
+    return renderModal(
+      props,
+      <Alert variant="info">
+        {translate('project_activity.graphs.data_table.no_data_warning')}
+      </Alert>
+    );
+  }
+
+  const tableData = series.reduce((acc, serie) => {
+    const data = filter(
+      serie.data,
+      // Make sure we respect the date filtering. On the graph, this is done by dynamically
+      // "zooming" on the series. Here, we actually have to "cut off" part of the serie's
+      // data points.
+      ({ x }) => {
+        if (graphEndDate && x > graphEndDate) {
+          return false;
+        }
+        if (graphStartDate && x < graphStartDate) {
+          return false;
+        }
+        return true;
+      }
+    );
+
+    data.forEach(({ x, y }) => {
+      const key = x.getTime();
+      if (acc[key] === undefined) {
+        acc[key] = { date: x } as DataTableEntry;
+      }
+
+      if (y !== undefined && !(typeof y === 'number' && isNaN(y))) {
+        acc[key][serie.name] = formatMeasure(y, serie.type);
+      }
+    });
+
+    return acc;
+  }, {} as { [x: number]: DataTableEntry });
+
+  const metrics = series.map(({ name }) => name);
+  const rows = slice(
+    sortBy(Object.values(tableData), ({ date }) => -date),
+    0,
+    MAX_DATA_TABLE_ROWS
+  ).map(({ date, ...values }) => (
+    <tr key={date.getTime()}>
+      <td className="nowrap">
+        <DateFormatter long={true} date={date} />
+        <div className="small note">
+          <TimeFormatter date={date} />
+        </div>
+      </td>
+      {metrics.map(metric => (
+        <td key={metric} className="thin nowrap">
+          {values[metric] || '-'}
+        </td>
+      ))}
+      <td>
+        <ul>
+          {getAnalysisEventsForDate(analyses, date).map(event => (
+            <li className="little-spacer-bottom" key={event.key}>
+              <EventInner event={event} readonly={true} />
+            </li>
+          ))}
+        </ul>
+      </td>
+    </tr>
+  ));
+
+  const rowCount = rows.length;
+
+  if (rowCount === 0) {
+    const start = graphStartDate && <DateFormatter long={true} date={graphStartDate} />;
+    const end = graphEndDate && <DateFormatter long={true} date={graphEndDate} />;
+    let suffix = '';
+    if (start && end) {
+      suffix = '_x_y';
+    } else if (start) {
+      suffix = '_x';
+    } else if (end) {
+      suffix = '_y';
+    }
+    return renderModal(
+      props,
+      <Alert variant="info">
+        <FormattedMessage
+          defaultMessage={translate(
+            `project_activity.graphs.data_table.no_data_warning_check_dates${suffix}`
+          )}
+          id={`project_activity.graphs.data_table.no_data_warning_check_dates${suffix}`}
+          values={{ start, end }}
+        />
+      </Alert>
+    );
+  }
+
+  return renderModal(
+    props,
+    <>
+      {rowCount === MAX_DATA_TABLE_ROWS && (
+        <Alert variant="info">
+          {translateWithParameters(
+            'project_activity.graphs.data_table.max_lines_warning',
+            MAX_DATA_TABLE_ROWS
+          )}
+        </Alert>
+      )}
+      <table className="spacer-top data zebra">
+        <thead>
+          <tr>
+            <th>{translate('date')}</th>
+            {series.map(serie => (
+              <th key={serie.name} className="thin nowrap">
+                {serie.translatedName}
+              </th>
+            ))}
+            <th>{translate('events')}</th>
+          </tr>
+        </thead>
+        <tbody>{rows}</tbody>
+      </table>
+    </>
+  );
+}
+
+function renderModal(props: DataTableModalProps, children: React.ReactNode) {
+  const heading = translate('project_activity.graphs.data_table.title');
+  return (
+    <Modal onRequestClose={props.onClose} contentLabel={heading} size="medium">
+      <div className="modal-head">
+        <h2>{heading}</h2>
+      </div>
+      <div className="modal-body modal-container">{children}</div>
+      <div className="modal-foot">
+        <Button onClick={props.onClose}>{translate('close')}</Button>
+      </div>
+    </Modal>
+  );
+}
index 33117b15773dd26a5d35c6eb2a591f822277b49a..f19056711d39186b5f720b4ff96baf869cd806ef 100644 (file)
 import * as React from 'react';
 import { AutoSizer } from 'react-virtualized/dist/commonjs/AutoSizer';
 import AdvancedTimeline from '../../components/charts/AdvancedTimeline';
+import { translate } from '../../helpers/l10n';
 import { formatMeasure, getShortType } from '../../helpers/measures';
 import { MeasureHistory, Serie } from '../../types/project-activity';
-import { AnalysisEvent } from '../../types/types';
+import { ParsedAnalysis } from '../../types/types';
+import { Button } from '../controls/buttons';
+import ModalButton from '../controls/ModalButton';
+import DataTableModal from './DataTableModal';
 import GraphsLegendCustom from './GraphsLegendCustom';
 import GraphsLegendStatic from './GraphsLegendStatic';
 import GraphsTooltips from './GraphsTooltips';
+import { getAnalysisEventsForDate } from './utils';
 
 interface Props {
-  events: AnalysisEvent[];
+  analyses: ParsedAnalysis[];
+  canShowDataAsTable?: boolean;
   graph: string;
   graphEndDate?: Date;
   graphStartDate?: Date;
@@ -69,7 +75,8 @@ export default class GraphHistory extends React.PureComponent<Props, State> {
 
   render() {
     const {
-      events,
+      analyses,
+      canShowDataAsTable = true,
       graph,
       graphEndDate,
       graphStartDate,
@@ -83,6 +90,7 @@ export default class GraphHistory extends React.PureComponent<Props, State> {
       graphDescription
     } = this.props;
     const { tooltipIdx, tooltipXPos } = this.state;
+    const events = getAnalysisEventsForDate(analyses, selectedDate);
 
     return (
       <div className="activity-graph-container flex-grow display-flex-column display-flex-stretch display-flex-justify-center">
@@ -132,6 +140,24 @@ export default class GraphHistory extends React.PureComponent<Props, State> {
             )}
           </AutoSizer>
         </div>
+        {canShowDataAsTable && (
+          <ModalButton
+            modal={({ onClose }) => (
+              <DataTableModal
+                analyses={analyses}
+                graphEndDate={graphEndDate}
+                graphStartDate={graphStartDate}
+                series={series}
+                onClose={onClose}
+              />
+            )}>
+            {({ onClick }) => (
+              <Button className="a11y-hidden" onClick={onClick}>
+                {translate('project_activity.graphs.open_in_table')}
+              </Button>
+            )}
+          </ModalButton>
+        )}
       </div>
     );
   }
index f0d4a97f820f595879600f44f9b1fda09135b47f..288bd4773b329cb6cf92412acd9a5d1c242bc988 100644 (file)
@@ -31,6 +31,7 @@ import { getSeriesMetricType, hasHistoryData, isCustomGraph } from './utils';
 interface Props {
   analyses: ParsedAnalysis[];
   ariaLabel?: string;
+  canShowDataAsTable?: boolean;
   graph: GraphType;
   graphs: Serie[][];
   graphEndDate?: Date;
@@ -63,31 +64,19 @@ export default class GraphsHistory extends React.PureComponent<Props, State> {
     }
   }
 
-  getSelectedDateEvents = () => {
-    const { selectedDate } = this.state;
-    const { analyses } = this.props;
-    if (analyses && selectedDate) {
-      const analysis = analyses.find(a => a.date.valueOf() === selectedDate.valueOf());
-      if (analysis) {
-        return analysis.events;
-      }
-    }
-    return [];
-  };
-
   updateTooltip = (selectedDate?: Date) => {
     this.setState({ selectedDate });
   };
 
   render() {
-    const { graph, loading, series, ariaLabel } = this.props;
+    const { analyses, graph, loading, series, ariaLabel, canShowDataAsTable } = this.props;
     const isCustom = isCustomGraph(graph);
 
     if (loading) {
       return (
         <div className="activity-graph-container flex-grow display-flex-column display-flex-stretch display-flex-justify-center">
           <div className="text-center">
-            <DeferredSpinner loading={loading} />
+            <DeferredSpinner ariaLabel={translate('loading')} loading={loading} />
           </div>
         </div>
       );
@@ -114,14 +103,14 @@ export default class GraphsHistory extends React.PureComponent<Props, State> {
         </div>
       );
     }
-    const events = this.getSelectedDateEvents();
     const showAreas = [GraphType.coverage, GraphType.duplications].includes(graph);
     return (
       <div className="display-flex-justify-center display-flex-column display-flex-stretch flex-grow">
         {this.props.graphs.map((graphSeries, idx) => {
           return (
             <GraphHistory
-              events={events}
+              analyses={analyses}
+              canShowDataAsTable={canShowDataAsTable}
               graph={graph}
               graphEndDate={this.props.graphEndDate}
               graphStartDate={this.props.graphStartDate}
index 921fd843e6abbbac66102d656e6baa579f197944..4c5b61eb82a58cbf396c054275d649377be2eef4 100644 (file)
@@ -23,7 +23,7 @@ import ZoomTimeLine from '../../components/charts/ZoomTimeLine';
 import { Serie } from '../../types/project-activity';
 import { hasHistoryData } from './utils';
 
-interface Props {
+interface GraphsZoomProps {
   graphEndDate?: Date;
   graphStartDate?: Date;
   leakPeriodDate?: Date;
@@ -34,13 +34,14 @@ interface Props {
   updateGraphZoom: (from?: Date, to?: Date) => void;
 }
 
-export default function GraphsZoom(props: Props) {
+export default function GraphsZoom(props: GraphsZoomProps) {
   if (props.loading || !hasHistoryData(props.series)) {
     return null;
   }
 
   return (
-    <div className="activity-graph-zoom">
+    // We hide this for screen readers; they should use date inputs instead.
+    <div className="activity-graph-zoom" aria-hidden={true}>
       <AutoSizer disableHeight={true}>
         {({ width }) => (
           <ZoomTimeLine
index bcab20a45b04294fe4f96f136ed6d461985d967f..5c0af0d142523dc24636fbac472073b578f2e04b 100644 (file)
@@ -36,6 +36,19 @@ exports[`should correctly render a graph 1`] = `
       <Component />
     </AutoSizer>
   </div>
+  <div
+    className="spacer-top big-spacer-bottom small"
+  >
+    <div
+      className="display-flex-justify-center"
+    >
+      <ModalButton
+        modal={[Function]}
+      >
+        <Component />
+      </ModalButton>
+    </div>
+  </div>
 </div>
 `;
 
@@ -76,5 +89,18 @@ exports[`should correctly render a graph: custom 1`] = `
       <Component />
     </AutoSizer>
   </div>
+  <div
+    className="spacer-top big-spacer-bottom small"
+  >
+    <div
+      className="display-flex-justify-center"
+    >
+      <ModalButton
+        modal={[Function]}
+      >
+        <Component />
+      </ModalButton>
+    </div>
+  </div>
 </div>
 `;
index b7d7f3fadc749b16b43a79fa0784cd77ff19514e..1bb06610746cf6c1faa2859a1fedc45c967ddf50 100644 (file)
@@ -5,8 +5,43 @@ exports[`should correctly render a graph 1`] = `
   className="display-flex-justify-center display-flex-column display-flex-stretch flex-grow"
 >
   <GraphHistory
+    analyses={
+      Array [
+        Object {
+          "date": 2016-10-27T14:33:50.000Z,
+          "events": Array [
+            Object {
+              "category": "VERSION",
+              "key": "E1",
+              "name": "6.5-SNAPSHOT",
+            },
+          ],
+          "key": "A1",
+        },
+        Object {
+          "date": 2016-10-27T10:21:15.000Z,
+          "events": Array [],
+          "key": "A2",
+        },
+        Object {
+          "date": 2016-10-26T10:17:29.000Z,
+          "events": Array [
+            Object {
+              "category": "OTHER",
+              "key": "E2",
+              "name": "foo",
+            },
+            Object {
+              "category": "VERSION",
+              "key": "E3",
+              "name": "6.4",
+            },
+          ],
+          "key": "A3",
+        },
+      ]
+    }
     ariaLabel="project_activity.graphs.explanation_x.metric.bugs.name"
-    events={Array []}
     graph="issues"
     isCustom={false}
     key="0"
@@ -50,8 +85,43 @@ exports[`should correctly render multiple graphs 1`] = `
   className="display-flex-justify-center display-flex-column display-flex-stretch flex-grow"
 >
   <GraphHistory
+    analyses={
+      Array [
+        Object {
+          "date": 2016-10-27T14:33:50.000Z,
+          "events": Array [
+            Object {
+              "category": "VERSION",
+              "key": "E1",
+              "name": "6.5-SNAPSHOT",
+            },
+          ],
+          "key": "A1",
+        },
+        Object {
+          "date": 2016-10-27T10:21:15.000Z,
+          "events": Array [],
+          "key": "A2",
+        },
+        Object {
+          "date": 2016-10-26T10:17:29.000Z,
+          "events": Array [
+            Object {
+              "category": "OTHER",
+              "key": "E2",
+              "name": "foo",
+            },
+            Object {
+              "category": "VERSION",
+              "key": "E3",
+              "name": "6.4",
+            },
+          ],
+          "key": "A3",
+        },
+      ]
+    }
     ariaLabel="project_activity.graphs.explanation_x.metric.bugs.name"
-    events={Array []}
     graph="issues"
     isCustom={false}
     key="0"
@@ -88,8 +158,43 @@ exports[`should correctly render multiple graphs 1`] = `
     updateTooltip={[Function]}
   />
   <GraphHistory
+    analyses={
+      Array [
+        Object {
+          "date": 2016-10-27T14:33:50.000Z,
+          "events": Array [
+            Object {
+              "category": "VERSION",
+              "key": "E1",
+              "name": "6.5-SNAPSHOT",
+            },
+          ],
+          "key": "A1",
+        },
+        Object {
+          "date": 2016-10-27T10:21:15.000Z,
+          "events": Array [],
+          "key": "A2",
+        },
+        Object {
+          "date": 2016-10-26T10:17:29.000Z,
+          "events": Array [
+            Object {
+              "category": "OTHER",
+              "key": "E2",
+              "name": "foo",
+            },
+            Object {
+              "category": "VERSION",
+              "key": "E3",
+              "name": "6.4",
+            },
+          ],
+          "key": "A3",
+        },
+      ]
+    }
     ariaLabel="project_activity.graphs.explanation_x.metric.bugs.name"
-    events={Array []}
     graph="issues"
     isCustom={false}
     key="1"
index 141c3d5c479d2ca898f89193e7bb04dd747053ae..46e3714d69615fa2c15f7a5059ccc14304d769e4 100644 (file)
@@ -23,7 +23,7 @@ import { localizeMetric } from '../../helpers/measures';
 import { get, save } from '../../helpers/storage';
 import { MetricKey } from '../../types/metrics';
 import { GraphType, MeasureHistory, Serie } from '../../types/project-activity';
-import { Dict, Metric } from '../../types/types';
+import { Dict, Metric, ParsedAnalysis } from '../../types/types';
 
 export const DEFAULT_GRAPH = GraphType.issues;
 
@@ -159,6 +159,16 @@ export function getActivityGraph(
   };
 }
 
+export function getAnalysisEventsForDate(analyses: ParsedAnalysis[], date?: Date) {
+  if (date) {
+    const analysis = analyses.find(a => a.date.valueOf() === date.valueOf());
+    if (analysis) {
+      return analysis.events;
+    }
+  }
+  return [];
+}
+
 function findMetric(key: string, metrics: Metric[] | Dict<Metric>) {
   if (Array.isArray(metrics)) {
     return metrics.find(metric => metric.key === key);
index 76dc91099ceec0e1dcbf9e7bac223c8d713ab55f..c79c05f3719fefcf84e465f2445358cdbc9d7874 100644 (file)
@@ -112,6 +112,7 @@ list_of_issues=List of issues
 list_of_projects=List of projects
 load_more=Load more
 load_verb=Load
+loading=Loading
 login=Login
 major=Major
 manual=Manual
@@ -1551,6 +1552,13 @@ project_activity.graphs.custom.no_history=There isn't enough data to generate an
 project_activity.graphs.custom.metric_no_history=This metric has no historical data to display.
 project_activity.graphs.custom.search=Search for a metric by name
 project_activity.graphs.custom.type_x_message=Only "{0}" metrics are available with your current selection.
+project_activity.graphs.open_in_table=Show the graph data in a table
+project_activity.graphs.data_table.title=Graph data in table format
+project_activity.graphs.data_table.max_lines_warning=Only the {0} most recent data entries are shown. If you want to see different data, change the date filters on the main page.
+project_activity.graphs.data_table.no_data_warning=There is no data for the selected series.
+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.custom_metric.covered_lines=Covered Lines