123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347 |
- /*
- * 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<Metric>;
- }
-
- class ComponentMeasuresApp extends React.PureComponent<Props, State> {
- 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<Query>) => {
- 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 (
- <StyledMain className="sw-rounded-1 sw-mb-4">
- <MeasureOverviewContainer
- branchLike={branchLike}
- domain={query.metric}
- leakPeriod={leakPeriod}
- metrics={this.state.metrics}
- rootComponent={component}
- router={this.props.router}
- selected={query.selected}
- updateQuery={this.updateQuery}
- />
- </StyledMain>
- );
- }
-
- if (!metric) {
- return (
- <StyledMain className="sw-rounded-1 sw-p-6 sw-mb-4">
- <MeasuresEmpty />
- </StyledMain>
- );
- }
-
- const hideDrilldown =
- isPullRequest(branchLike) &&
- (metric.key === MetricKey.coverage || metric.key === MetricKey.duplicated_lines_density);
-
- if (hideDrilldown) {
- return (
- <StyledMain className="sw-rounded-1 sw-p-6 sw-mb-4">
- <Note>{translate('component_measures.details_are_not_available')}</Note>
- </StyledMain>
- );
- }
-
- return (
- <StyledMain className="sw-rounded-1 sw-mb-4">
- <MeasureContent
- asc={query.asc}
- branchLike={branchLike}
- leakPeriod={leakPeriod}
- metrics={this.state.metrics}
- requestedMetric={metric}
- rootComponent={component}
- router={this.props.router}
- selected={query.selected}
- updateQuery={this.updateQuery}
- view={query.view}
- />
- </StyledMain>
- );
- };
-
- 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 (
- <LargeCenteredLayout id="component-measures" className="sw-pt-8">
- <Suggestions suggestions="component_measures" />
- <Helmet defer={false} title={translate('layout.measures')} />
- <PageContentFontWrapper className="sw-body-sm">
- <Spinner isLoading={this.state.loading} />
-
- {measures.length > 0 ? (
- <div className="sw-grid sw-grid-cols-12 sw-w-full">
- <Sidebar
- measures={measures}
- selectedMetric={metric ? metric.key : query.metric}
- showFullMeasures={showFullMeasures}
- updateQuery={this.updateQuery}
- />
-
- <div className="sw-col-span-9 sw-ml-12">
- {!canBrowseAllChildProjects && isPortfolioLike(qualifier) && (
- <FlagMessage className="sw-mb-4 it__portfolio_warning" variant="warning">
- {translate('component_measures.not_all_measures_are_shown')}
- <HelpTooltip
- className="sw-ml-2"
- overlay={translate('component_measures.not_all_measures_are_shown.help')}
- />
- </FlagMessage>
- )}
- {!areLeakAndOverallCCTMeasuresComputed(measures) && (
- <AnalysisMissingInfoMessage className="sw-mb-4" qualifier={qualifier} />
- )}
- {this.renderContent(displayOverview, query, metric)}
- </div>
- </div>
- ) : (
- <StyledMain className="sw-rounded-1 sw-p-6 sw-mb-4">
- <MeasuresEmpty />
- </StyledMain>
- )}
- </PageContentFontWrapper>
- </LargeCenteredLayout>
- );
- }
- }
-
- /*
- * 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 <WrappedApp branchLike={branchLike} component={component as ComponentMeasure} />;
- }
-
- export default AppWithComponentContext;
-
- const StyledMain = withTheme(styled.main`
- background-color: ${themeColor('pageBlock')};
- border: ${themeBorder('default', 'pageBlockBorder')};
- `);
|