123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293 |
- /*
- * 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<Query>) => 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<Props, State> {
- 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<Point>(
- 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<Date | undefined>) => {
- 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 (
- <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;
-
- return (
- <div className="sw-px-5 sw-py-4 sw-h-full sw-flex sw-flex-col sw-box-border">
- <GraphsHeader
- onAddCustomMetric={this.handleAddCustomMetric}
- className="sw-mb-4"
- graph={query.graph}
- metrics={metrics}
- metricsTypeFilter={this.getMetricsTypeFilter()}
- onRemoveCustomMetric={this.handleRemoveCustomMetric}
- selectedMetrics={query.customMetrics}
- onUpdateGraph={this.handleUpdateGraph}
- />
- {this.renderQualitiesMetricInfoMessage()}
- <GraphsHistory
- analyses={analyses}
- graph={query.graph}
- graphEndDate={graphEndDate}
- graphStartDate={graphStartDate}
- graphs={this.state.graphs}
- leakPeriodDate={leakPeriodDate}
- loading={loading}
- measuresHistory={measuresHistory}
- removeCustomMetric={this.handleRemoveCustomMetric}
- selectedDate={query.selectedDate}
- series={series}
- updateGraphZoom={this.handleUpdateGraphZoom}
- updateSelectedDate={this.handleUpdateSelectedDate}
- />
- <GraphsZoom
- graphEndDate={graphEndDate}
- graphStartDate={graphStartDate}
- leakPeriodDate={leakPeriodDate}
- loading={loading}
- metricsType={getSeriesMetricType(series)}
- series={series}
- showAreas={[GraphType.coverage, GraphType.duplications].includes(query.graph)}
- onUpdateGraphZoom={this.handleUpdateGraphZoom}
- />
- </div>
- );
- }
- }
|