/* * 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 { 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'; import { generateSeries, getActivityGraph, getDisplayedHistoryMetrics, getSeriesMetricType, isCustomGraph, 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, ParsedAnalysis, Point, Serie, } from '../../../types/project-activity'; import { Metric } from '../../../types/types'; import { Query, datesQueryChanged, historyQueryChanged } from '../utils'; import { PROJECT_ACTIVITY_GRAPH } from './ProjectActivityApp'; interface Props { analyses: ParsedAnalysis[]; leakPeriodDate?: Date; loading: boolean; measuresHistory: MeasureHistory[]; metrics: Metric[]; project: string; query: Query; updateQuery: (changes: Partial) => void; } interface State { graphStartDate?: Date; graphEndDate?: Date; series: Serie[]; graphs: Serie[][]; } const MAX_GRAPH_NB = 2; const MAX_SERIES_PER_GRAPH = 3; export default class ProjectActivityGraphs extends React.PureComponent { constructor(props: Props) { super(props); const series = generateSeries( props.measuresHistory, props.query.graph, props.metrics, getDisplayedHistoryMetrics(props.query.graph, props.query.customMetrics), ); this.state = { series, graphs: splitSeriesInGraphs(series, MAX_GRAPH_NB, MAX_SERIES_PER_GRAPH), ...this.getStateZoomDates(undefined, props, series), }; this.updateQueryDateRange = debounce(this.updateQueryDateRange, 500); } componentDidUpdate(prevProps: Props) { let newSeries; let newGraphs; if ( prevProps.measuresHistory !== this.props.measuresHistory || historyQueryChanged(prevProps.query, this.props.query) ) { newSeries = generateSeries( this.props.measuresHistory, this.props.query.graph, this.props.metrics, getDisplayedHistoryMetrics(this.props.query.graph, this.props.query.customMetrics), ); newGraphs = splitSeriesInGraphs(newSeries, MAX_GRAPH_NB, MAX_SERIES_PER_GRAPH); } const newDates = this.getStateZoomDates(prevProps, this.props, newSeries); if (newSeries || newDates) { let newState = {} as State; if (newSeries) { newState.series = newSeries; } if (newGraphs) { newState.graphs = newGraphs; } if (newDates) { newState = { ...newState, ...newDates }; } this.setState(newState); } } getStateZoomDates = (prevProps: Props | undefined, props: Props, newSeries?: Serie[]) => { const newDates = { from: props.query.from || undefined, to: props.query.to || undefined, }; if (!prevProps || datesQueryChanged(prevProps.query, props.query)) { return { graphEndDate: newDates.to, graphStartDate: newDates.from }; } if (newDates.to === undefined && newDates.from === undefined && newSeries !== undefined) { const firstValid = minBy( newSeries.map((serie) => serie.data.find((p) => Boolean(p.y || p.y === 0))), 'x', ); const lastValid = maxBy( newSeries.map((serie) => findLast(serie.data, (p) => Boolean(p.y || p.y === 0))!), 'x', ); return { graphEndDate: lastValid?.x, graphStartDate: firstValid?.x, }; } return null; }; getMetricsTypeFilter = () => { if (this.state.graphs.length < MAX_GRAPH_NB) { return undefined; } return this.state.graphs .filter((graph) => graph.length < MAX_SERIES_PER_GRAPH) .map((graph) => graph[0].type); }; handleAddCustomMetric = (metric: string) => { const customMetrics = [...this.props.query.customMetrics, metric]; saveActivityGraph(PROJECT_ACTIVITY_GRAPH, this.props.project, GraphType.custom, customMetrics); this.props.updateQuery({ customMetrics }); }; handleRemoveCustomMetric = (removedMetric: string) => { const customMetrics = this.props.query.customMetrics.filter( (metric) => metric !== removedMetric, ); saveActivityGraph(PROJECT_ACTIVITY_GRAPH, this.props.project, GraphType.custom, customMetrics); this.props.updateQuery({ customMetrics }); }; handleUpdateGraph = (graph: GraphType) => { saveActivityGraph(PROJECT_ACTIVITY_GRAPH, this.props.project, graph); if (isCustomGraph(graph) && this.props.query.customMetrics.length <= 0) { const { customGraphs } = getActivityGraph(PROJECT_ACTIVITY_GRAPH, this.props.project); this.props.updateQuery({ graph, customMetrics: customGraphs }); } else { this.props.updateQuery({ graph, customMetrics: [] }); } }; handleUpdateGraphZoom = (graphStartDate?: Date, graphEndDate?: Date) => { if (graphEndDate !== undefined && graphStartDate !== undefined) { const msDiff = Math.abs(graphEndDate.valueOf() - graphStartDate.valueOf()); // 12 hours minimum between the two dates if (msDiff < 1000 * 60 * 60 * 12) { return; } } this.setState({ graphStartDate, graphEndDate }); this.updateQueryDateRange([graphStartDate, graphEndDate]); }; handleUpdateSelectedDate = (selectedDate?: Date) => { this.props.updateQuery({ selectedDate }); }; updateQueryDateRange = (dates: Array) => { if (dates[0] === undefined || dates[1] === undefined) { this.props.updateQuery({ from: dates[0], to: dates[1] }); } else { const sortedDates = sortBy(dates); this.props.updateQuery({ from: sortedDates[0], to: sortedDates[1] }); } }; 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 ( {translate('learn_more')} ), }} /> ); } return null; }; render() { const { analyses, leakPeriodDate, loading, measuresHistory, metrics, query } = this.props; const { graphEndDate, graphStartDate, series } = this.state; return (
{this.renderQualitiesMetricInfoMessage()}
); } }