/* * 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 { withTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { Spinner } from '@sonarsource/echoes-react'; import { FlagMessage, LargeCenteredLayout, Note, PageContentFontWrapper, themeBorder, themeColor, } from 'design-system'; import { keyBy } from 'lodash'; import * as React from 'react'; import { Helmet } from 'react-helmet-async'; import { getMeasuresWithPeriod } from '../../../api/measures'; import { getAllMetrics } from '../../../api/metrics'; import { ComponentContext } from '../../../app/components/componentContext/ComponentContext'; import HelpTooltip from '../../../components/controls/HelpTooltip'; import Suggestions from '../../../components/embed-docs-modal/Suggestions'; import { Location, Router, withRouter } from '../../../components/hoc/withRouter'; import { enhanceMeasure } from '../../../components/measure/utils'; import '../../../components/search-navigator.css'; import AnalysisMissingInfoMessage from '../../../components/shared/AnalysisMissingInfoMessage'; import { getBranchLikeQuery, isPullRequest, isSameBranchLike } from '../../../helpers/branch-like'; import { translate } from '../../../helpers/l10n'; import { areLeakAndOverallCCTMeasuresComputed } from '../../../helpers/measures'; import { WithBranchLikesProps, useBranchesQuery } from '../../../queries/branch'; import { ComponentQualifier, isPortfolioLike } from '../../../types/component'; import { MeasurePageView } from '../../../types/measures'; import { MetricKey } from '../../../types/metrics'; import { ComponentMeasure, Dict, MeasureEnhanced, Metric, Period } from '../../../types/types'; import Sidebar from '../sidebar/Sidebar'; import { Query, banQualityGateMeasure, filterMeasures, getMeasuresPageMetricKeys, groupByDomains, hasBubbleChart, hasFullMeasures, hasTree, hasTreemap, parseQuery, serializeQuery, sortMeasures, } from '../utils'; import MeasureContent from './MeasureContent'; import MeasureOverviewContainer from './MeasureOverviewContainer'; import MeasuresEmpty from './MeasuresEmpty'; interface Props extends WithBranchLikesProps { component: ComponentMeasure; location: Location; router: Router; } interface State { leakPeriod?: Period; loading: boolean; measures: MeasureEnhanced[]; metrics: Dict; } class ComponentMeasuresApp extends React.PureComponent { mounted = false; state: State; constructor(props: Props) { super(props); this.state = { loading: true, measures: [], metrics: {}, }; } componentDidMount() { this.mounted = true; getAllMetrics().then( (metrics) => { const byKey = keyBy(metrics, 'key'); this.setState({ metrics: byKey }); }, () => {}, ); } componentDidUpdate(prevProps: Props, prevState: State) { const prevQuery = parseQuery(prevProps.location.query); const query = parseQuery(this.props.location.query); const hasSelectedQueryChanged = prevQuery.selected !== query.selected; const hasBranchChanged = !isSameBranchLike(prevProps.branchLike, this.props.branchLike); const isBranchReady = isPortfolioLike(this.props.component.qualifier) || this.props.branchLike !== undefined; const haveMetricsChanged = Object.keys(this.state.metrics).length !== Object.keys(prevState.metrics).length; const areMetricsReady = Object.keys(this.state.metrics).length > 0; if ( areMetricsReady && isBranchReady && (haveMetricsChanged || hasBranchChanged || hasSelectedQueryChanged) ) { this.fetchMeasures(this.state.metrics); } } componentWillUnmount() { this.mounted = false; } fetchMeasures(metrics: State['metrics']) { const { branchLike } = this.props; const query = parseQuery(this.props.location.query); const componentKey = query.selected !== undefined && query.selected !== '' ? query.selected : this.props.component.key; const filteredKeys = getMeasuresPageMetricKeys(metrics, branchLike); getMeasuresWithPeriod(componentKey, filteredKeys, getBranchLikeQuery(branchLike)).then( ({ component, period }) => { if (this.mounted) { const measures = filterMeasures( banQualityGateMeasure(component).map((measure) => enhanceMeasure(measure, metrics)), ); const leakPeriod = component.qualifier === ComponentQualifier.Project ? period : undefined; this.setState({ loading: false, leakPeriod, measures: measures.filter( (measure) => measure.value !== undefined || measure.leak !== undefined, ), }); } }, () => { if (this.mounted) { this.setState({ loading: false }); } }, ); } getSelectedMetric = (query: Query, displayOverview: boolean) => { if (displayOverview) { return undefined; } const metric = this.state.metrics[query.metric]; if (!metric) { const domainMeasures = groupByDomains(this.state.measures); const firstMeasure = domainMeasures[0] && sortMeasures(domainMeasures[0].name, domainMeasures[0].measures)[0]; if (firstMeasure && typeof firstMeasure !== 'string') { return firstMeasure.metric; } } return metric; }; updateQuery = (newQuery: Partial) => { const query: Query = { ...parseQuery(this.props.location.query), ...newQuery }; const metric = this.getSelectedMetric(query, false); if (metric) { if (query.view === MeasurePageView.treemap && !hasTreemap(metric.key, metric.type)) { query.view = MeasurePageView.tree; } else if (query.view === MeasurePageView.tree && !hasTree(metric.key)) { query.view = MeasurePageView.list; } } this.props.router.push({ pathname: this.props.location.pathname, query: { ...serializeQuery(query), ...getBranchLikeQuery(this.props.branchLike), id: this.props.component.key, }, }); }; renderContent = (displayOverview: boolean, query: Query, metric?: Metric) => { const { branchLike, component } = this.props; const { leakPeriod } = this.state; if (displayOverview) { return ( ); } if (!metric) { return ( ); } const hideDrilldown = isPullRequest(branchLike) && (metric.key === MetricKey.coverage || metric.key === MetricKey.duplicated_lines_density); if (hideDrilldown) { return ( {translate('component_measures.details_are_not_available')} ); } return ( ); }; render() { const { branchLike } = this.props; const { measures } = this.state; const { canBrowseAllChildProjects, qualifier } = this.props.component; const query = parseQuery(this.props.location.query); const showFullMeasures = hasFullMeasures(branchLike); const displayOverview = hasBubbleChart(query.metric); const metric = this.getSelectedMetric(query, displayOverview); return ( {measures.length > 0 ? (
{!canBrowseAllChildProjects && isPortfolioLike(qualifier) && ( {translate('component_measures.not_all_measures_are_shown')} )} {!areLeakAndOverallCCTMeasuresComputed(measures) && ( )} {this.renderContent(displayOverview, query, metric)}
) : ( )}
); } } /* * This needs to be refactored: the issue * is that we can't use the usual withComponentContext HOC, because the type * of `component` isn't the same. It probably used to work because of the lazy loading */ const WrappedApp = withRouter(ComponentMeasuresApp); function AppWithComponentContext() { const { component } = React.useContext(ComponentContext); const { data: { branchLike } = {} } = useBranchesQuery(component); return ; } export default AppWithComponentContext; const StyledMain = withTheme(styled.main` background-color: ${themeColor('pageBlock')}; border: ${themeBorder('default', 'pageBlockBorder')}; `);