aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx
diff options
context:
space:
mode:
authorWouter Admiraal <wouter.admiraal@sonarsource.com>2019-12-19 10:58:37 +0100
committerSonarTech <sonartech@sonarsource.com>2020-02-10 20:46:15 +0100
commit822405efd0ddde6b1e123c40cc1255e9b57c05f8 (patch)
tree5896c4f40e0240c563f48370b90b2450a0a061a8 /server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx
parent914f3a4e69b2d5896bf71e71ee63f45d45303efe (diff)
downloadsonarqube-822405efd0ddde6b1e123c40cc1255e9b57c05f8.tar.gz
sonarqube-822405efd0ddde6b1e123c40cc1255e9b57c05f8.zip
SONAR-12632 SONAR-12678 Implement new Overview layout
Diffstat (limited to 'server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx')
-rw-r--r--server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx410
1 files changed, 410 insertions, 0 deletions
diff --git a/server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx b/server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx
new file mode 100644
index 00000000000..6ad66b9295d
--- /dev/null
+++ b/server/sonar-web/src/main/js/apps/overview/branches/BranchOverview.tsx
@@ -0,0 +1,410 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2020 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 { sortBy, uniq } from 'lodash';
+import * as React from 'react';
+import { parseDate, toNotSoISOString } from 'sonar-ui-common/helpers/dates';
+import { isDefined } from 'sonar-ui-common/helpers/types';
+import { getApplicationLeak } from '../../../api/application';
+import { getMeasuresAndMeta } from '../../../api/measures';
+import { getProjectActivity } from '../../../api/projectActivity';
+import { getApplicationQualityGate, getQualityGateProjectStatus } from '../../../api/quality-gates';
+import { getTimeMachineData } from '../../../api/time-machine';
+import {
+ getActivityGraph,
+ getHistoryMetrics,
+ saveActivityGraph
+} from '../../../components/activity-graph/utils';
+import {
+ getBranchLikeDisplayName,
+ getBranchLikeQuery,
+ isSameBranchLike
+} from '../../../helpers/branch-like';
+import { enhanceConditionWithMeasure, enhanceMeasuresWithMetrics } from '../../../helpers/measures';
+import { getLeakPeriod } from '../../../helpers/periods';
+import {
+ extractStatusConditionsFromApplicationStatusChildProject,
+ extractStatusConditionsFromProjectStatus
+} from '../../../helpers/qualityGates';
+import { ApplicationPeriod } from '../../../types/application';
+import { BranchLike } from '../../../types/branch-like';
+import { MetricKey } from '../../../types/metrics';
+import { GraphType, MeasureHistory } from '../../../types/project-activity';
+import { QualityGateStatus, QualityGateStatusCondition } from '../../../types/quality-gates';
+import '../styles.css';
+import { HISTORY_METRICS_LIST, METRICS } from '../utils';
+import BranchOverviewRenderer from './BranchOverviewRenderer';
+
+interface Props {
+ branchLike?: BranchLike;
+ component: T.Component;
+}
+
+interface State {
+ analyses?: T.Analysis[];
+ appLeak?: ApplicationPeriod;
+ graph: GraphType;
+ loadingHistory?: boolean;
+ loadingStatus?: boolean;
+ measures?: T.MeasureEnhanced[];
+ measuresHistory?: MeasureHistory[];
+ metrics?: T.Metric[];
+ periods?: T.Period[];
+ qgStatuses?: QualityGateStatus[];
+}
+
+export const BRANCH_OVERVIEW_ACTIVITY_GRAPH = 'sonar_branch_overview.graph';
+
+// Get all history data over the past year.
+const FROM_DATE = toNotSoISOString(new Date().setFullYear(new Date().getFullYear() - 1));
+
+export default class BranchOverview extends React.PureComponent<Props, State> {
+ mounted = false;
+ state: State;
+
+ constructor(props: Props) {
+ super(props);
+
+ const { graph } = getActivityGraph(BRANCH_OVERVIEW_ACTIVITY_GRAPH, props.component.key);
+ this.state = { graph };
+ }
+
+ componentDidMount() {
+ this.mounted = true;
+ this.loadStatus();
+ this.loadHistory();
+ }
+
+ componentDidUpdate(prevProps: Props) {
+ if (
+ this.props.component.key !== prevProps.component.key ||
+ !isSameBranchLike(this.props.branchLike, prevProps.branchLike)
+ ) {
+ this.loadStatus();
+ this.loadHistory();
+ }
+ }
+
+ componentWillUnmount() {
+ this.mounted = false;
+ }
+
+ loadStatus = () => {
+ if (this.props.component.qualifier === 'APP') {
+ this.loadApplicationStatus();
+ } else {
+ this.loadProjectStatus();
+ }
+ };
+
+ loadApplicationStatus = async () => {
+ const { branchLike, component } = this.props;
+ this.setState({ loadingStatus: true });
+
+ // Start by loading the application quality gate info, as well as the meta
+ // data for the application as a whole.
+ const appStatus = await getApplicationQualityGate({
+ application: component.key,
+ ...getBranchLikeQuery(branchLike)
+ });
+ const { measures: appMeasures, metrics, periods } = await this.loadMeasuresAndMeta(
+ component.key
+ );
+
+ // We also need to load the application leak periods separately.
+ getApplicationLeak(component.key, branchLike && getBranchLikeDisplayName(branchLike)).then(
+ leaks => {
+ if (this.mounted && leaks && leaks.length) {
+ const sortedLeaks = sortBy(leaks, leak => {
+ return new Date(leak.date);
+ });
+ this.setState({
+ appLeak: sortedLeaks[0]
+ });
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ appLeak: undefined });
+ }
+ }
+ );
+
+ // We need to load the measures for each project in an application
+ // individually, in order to display all QG conditions correctly. Loading
+ // them at the parent application level will not get all the necessary
+ // information, unfortunately, as they are aggregated.
+ Promise.all(
+ appStatus.projects.map(project => {
+ return this.loadMeasuresAndMeta(
+ project.key,
+ // Only load metrics that apply to failing QG conditions; we don't
+ // need the others anyway.
+ project.conditions.filter(c => c.status !== 'OK').map(c => c.metric)
+ ).then(({ measures }) => ({
+ measures,
+ project
+ }));
+ })
+ ).then(
+ results => {
+ if (this.mounted) {
+ const qgStatuses = results.map(({ measures = [], project }) => {
+ const { key, name, status } = project;
+ const conditions = extractStatusConditionsFromApplicationStatusChildProject(project);
+ const failedConditions = this.getFailedConditions(conditions, measures);
+
+ return {
+ failedConditions,
+ key,
+ name,
+ status
+ };
+ });
+
+ this.setState({
+ loadingStatus: false,
+ measures: appMeasures,
+ metrics,
+ periods,
+ qgStatuses
+ });
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ loadingStatus: false, qgStatuses: undefined });
+ }
+ }
+ );
+ };
+
+ loadProjectStatus = async () => {
+ const {
+ branchLike,
+ component: { key, name }
+ } = this.props;
+ this.setState({ loadingStatus: true });
+
+ const projectStatus = await getQualityGateProjectStatus({
+ projectKey: key,
+ ...getBranchLikeQuery(branchLike)
+ });
+
+ // Get failing condition metric keys. We need measures for them as well to
+ // render them.
+ const metricKeys =
+ projectStatus.conditions !== undefined
+ ? uniq([...METRICS, ...projectStatus.conditions.map(c => c.metricKey)])
+ : METRICS;
+
+ this.loadMeasuresAndMeta(key, metricKeys).then(
+ ({ measures, metrics, periods }) => {
+ if (this.mounted && measures) {
+ const { ignoredConditions, status } = projectStatus;
+ const conditions = extractStatusConditionsFromProjectStatus(projectStatus);
+ const failedConditions = this.getFailedConditions(conditions, measures);
+
+ const qgStatus = {
+ ignoredConditions,
+ failedConditions,
+ key,
+ name,
+ status
+ };
+
+ this.setState({
+ loadingStatus: false,
+ measures,
+ metrics,
+ periods,
+ qgStatuses: [qgStatus]
+ });
+ } else if (this.mounted) {
+ this.setState({ loadingStatus: false, qgStatuses: undefined });
+ }
+ },
+ () => {
+ if (this.mounted) {
+ this.setState({ loadingStatus: false, qgStatuses: undefined });
+ }
+ }
+ );
+ };
+
+ loadMeasuresAndMeta = (componentKey: string, metricKeys: string[] = []) => {
+ const { branchLike } = this.props;
+
+ return getMeasuresAndMeta(componentKey, metricKeys.length > 0 ? metricKeys : METRICS, {
+ additionalFields: 'metrics,periods',
+ ...getBranchLikeQuery(branchLike)
+ }).then(({ component: { measures }, metrics, periods }) => {
+ return {
+ measures: enhanceMeasuresWithMetrics(measures || [], metrics || []),
+ metrics,
+ periods
+ };
+ });
+ };
+
+ loadHistory = () => {
+ this.setState({ loadingHistory: true });
+
+ return Promise.all([this.loadHistoryMeasures(), this.loadAnalyses()]).then(
+ this.doneLoadingHistory,
+ this.doneLoadingHistory
+ );
+ };
+
+ loadHistoryMeasures = () => {
+ const { branchLike, component } = this.props;
+ const { graph } = this.state;
+
+ const graphMetrics = getHistoryMetrics(graph, []);
+ const metrics = uniq([...HISTORY_METRICS_LIST, ...graphMetrics]);
+
+ return getTimeMachineData({
+ ...getBranchLikeQuery(branchLike),
+ from: FROM_DATE,
+ component: component.key,
+ metrics: metrics.join()
+ }).then(
+ ({ measures }) => {
+ if (this.mounted) {
+ this.setState({
+ measuresHistory: measures.map(measure => ({
+ metric: measure.metric,
+ history: measure.history.map(analysis => ({
+ date: parseDate(analysis.date),
+ value: analysis.value
+ }))
+ }))
+ });
+ }
+ },
+ () => {}
+ );
+ };
+
+ loadAnalyses = () => {
+ const { branchLike } = this.props;
+
+ return getProjectActivity({
+ ...getBranchLikeQuery(branchLike),
+ project: this.getTopLevelComponent(),
+ from: FROM_DATE
+ }).then(
+ ({ analyses }) => {
+ if (this.mounted) {
+ this.setState({
+ analyses
+ });
+ }
+ },
+ () => {}
+ );
+ };
+
+ getFailedConditions = (
+ conditions: QualityGateStatusCondition[],
+ measures: T.MeasureEnhanced[]
+ ) => {
+ return (
+ conditions
+ .filter(c => c.level !== 'OK')
+ // Enhance them with Metric information, which will be needed
+ // to render the conditions properly.
+ .map(c => enhanceConditionWithMeasure(c, measures))
+ // The enhancement will return undefined if it cannot find the
+ // appropriate measure. Make sure we filter them out.
+ .filter(isDefined)
+ );
+ };
+
+ getTopLevelComponent = () => {
+ const { component } = this.props;
+ let current = component.breadcrumbs.length - 1;
+ while (
+ current > 0 &&
+ !['TRK', 'VW', 'APP'].includes(component.breadcrumbs[current].qualifier)
+ ) {
+ current--;
+ }
+ return component.breadcrumbs[current].key;
+ };
+
+ doneLoadingHistory = () => {
+ if (this.mounted) {
+ this.setState({
+ loadingHistory: false
+ });
+ }
+ };
+
+ handleGraphChange = (graph: GraphType) => {
+ const { component } = this.props;
+ saveActivityGraph(BRANCH_OVERVIEW_ACTIVITY_GRAPH, component.key, graph);
+ this.setState({ graph, loadingHistory: true }, () => {
+ this.loadHistoryMeasures().then(this.doneLoadingHistory, this.doneLoadingHistory);
+ });
+ };
+
+ render() {
+ const { branchLike, component } = this.props;
+ const {
+ analyses,
+ appLeak,
+ graph,
+ loadingStatus,
+ loadingHistory,
+ measures,
+ measuresHistory,
+ metrics,
+ periods,
+ qgStatuses
+ } = this.state;
+
+ const leakPeriod = component.qualifier === 'APP' ? appLeak : getLeakPeriod(periods);
+
+ const projectIsEmpty =
+ loadingStatus === false &&
+ (measures === undefined ||
+ measures.find(measure =>
+ ([MetricKey.lines, MetricKey.new_lines] as string[]).includes(measure.metric.key)
+ ) === undefined);
+
+ return (
+ <BranchOverviewRenderer
+ analyses={analyses}
+ branchLike={branchLike}
+ component={component}
+ graph={graph}
+ leakPeriod={leakPeriod}
+ loadingHistory={loadingHistory}
+ loadingStatus={loadingStatus}
+ measures={measures}
+ measuresHistory={measuresHistory}
+ metrics={metrics}
+ onGraphChange={this.handleGraphChange}
+ projectIsEmpty={projectIsEmpty}
+ qgStatuses={qgStatuses}
+ />
+ );
+ }
+}