'overview.activity.graph_shows_data_for_x',
displayedMetrics.map(metricKey => localizeMetric(metricKey)).join(', ')
)}
+ canShowDataAsTable={false}
graph={graph}
graphs={graphs}
leakPeriodDate={shownLeakPeriodDate}
<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 [
<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 [
--- /dev/null
+/*
+ * 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>
+ );
+}
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;
render() {
const {
- events,
+ analyses,
+ canShowDataAsTable = true,
graph,
graphEndDate,
graphStartDate,
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">
)}
</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>
);
}
interface Props {
analyses: ParsedAnalysis[];
ariaLabel?: string;
+ canShowDataAsTable?: boolean;
graph: GraphType;
graphs: Serie[][];
graphEndDate?: Date;
}
}
- 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>
);
</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}
import { Serie } from '../../types/project-activity';
import { hasHistoryData } from './utils';
-interface Props {
+interface GraphsZoomProps {
graphEndDate?: Date;
graphStartDate?: Date;
leakPeriodDate?: Date;
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
<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>
`;
<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>
`;
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"
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"
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"
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;
};
}
+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);
list_of_projects=List of projects
load_more=Load more
load_verb=Load
+loading=Loading
login=Login
major=Major
manual=Manual
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