You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

ProjectActivityApp.tsx 6.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2024 SonarSource SA
  4. * mailto:info AT sonarsource DOT com
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public
  8. * License as published by the Free Software Foundation; either
  9. * version 3 of the License, or (at your option) any later version.
  10. *
  11. * This program is distributed in the hope that it will be useful,
  12. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  14. * Lesser General Public License for more details.
  15. *
  16. * You should have received a copy of the GNU Lesser General Public License
  17. * along with this program; if not, write to the Free Software Foundation,
  18. * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  19. */
  20. import React from 'react';
  21. import {
  22. useComponent,
  23. useTopLevelComponentKey,
  24. } from '../../../app/components/componentContext/withComponentContext';
  25. import { useMetrics } from '../../../app/components/metrics/withMetricsContext';
  26. import {
  27. DEFAULT_GRAPH,
  28. getActivityGraph,
  29. getHistoryMetrics,
  30. isCustomGraph,
  31. } from '../../../components/activity-graph/utils';
  32. import { useLocation, useRouter } from '../../../components/hoc/withRouter';
  33. import { getBranchLikeQuery } from '../../../helpers/branch-like';
  34. import { HIDDEN_METRICS } from '../../../helpers/constants';
  35. import { parseDate } from '../../../helpers/dates';
  36. import useApplicationLeakQuery from '../../../queries/applications';
  37. import { useBranchesQuery } from '../../../queries/branch';
  38. import { useAllMeasuresHistoryQuery } from '../../../queries/measures';
  39. import { useAllProjectAnalysesQuery } from '../../../queries/project-analyses';
  40. import { isApplication, isPortfolioLike, isProject } from '../../../types/component';
  41. import { MetricKey } from '../../../types/metrics';
  42. import { MeasureHistory, ParsedAnalysis } from '../../../types/project-activity';
  43. import { Query, parseQuery, serializeUrlQuery } from '../utils';
  44. import ProjectActivityAppRenderer from './ProjectActivityAppRenderer';
  45. export interface State {
  46. analyses: ParsedAnalysis[];
  47. analysesLoading: boolean;
  48. leakPeriodDate?: Date;
  49. graphLoading: boolean;
  50. initialized: boolean;
  51. measuresHistory: MeasureHistory[];
  52. query: Query;
  53. }
  54. export const PROJECT_ACTIVITY_GRAPH = 'sonar_project_activity.graph';
  55. export function ProjectActivityApp() {
  56. const { query, pathname } = useLocation();
  57. const parsedQuery = parseQuery(query);
  58. const router = useRouter();
  59. const { component } = useComponent();
  60. const metrics = useMetrics();
  61. const { data: { branchLike } = {}, isFetching: isFetchingBranch } = useBranchesQuery(component);
  62. const enabled =
  63. component?.key !== undefined &&
  64. (isPortfolioLike(component?.qualifier) || (Boolean(branchLike) && !isFetchingBranch));
  65. const componentKey = useTopLevelComponentKey();
  66. const { data: appLeaks } = useApplicationLeakQuery(
  67. componentKey ?? '',
  68. isApplication(component?.qualifier),
  69. );
  70. const { data: analysesData, isLoading: isLoadingAnalyses } = useAllProjectAnalysesQuery(enabled);
  71. const { data: historyData, isLoading: isLoadingHistory } = useAllMeasuresHistoryQuery(
  72. componentKey,
  73. getBranchLikeQuery(branchLike),
  74. getHistoryMetrics(query.graph || DEFAULT_GRAPH, parsedQuery.customMetrics).join(','),
  75. enabled,
  76. );
  77. const analyses = React.useMemo(() => analysesData ?? [], [analysesData]);
  78. const measuresHistory = React.useMemo(
  79. () =>
  80. historyData?.measures?.map((measure) => ({
  81. metric: measure.metric,
  82. history: measure.history.map((historyItem) => ({
  83. date: parseDate(historyItem.date),
  84. value: historyItem.value,
  85. })),
  86. })) ?? [],
  87. [historyData],
  88. );
  89. const leakPeriodDate = React.useMemo(() => {
  90. if (appLeaks?.[0]) {
  91. return parseDate(appLeaks[0].date);
  92. } else if (isProject(component?.qualifier) && component?.leakPeriodDate !== undefined) {
  93. return parseDate(component.leakPeriodDate);
  94. }
  95. return undefined;
  96. }, [appLeaks, component?.leakPeriodDate, component?.qualifier]);
  97. const filteredMetrics = React.useMemo(() => {
  98. if (isPortfolioLike(component?.qualifier)) {
  99. return Object.values(metrics).filter(
  100. (metric) => metric.key !== MetricKey.security_hotspots_reviewed,
  101. );
  102. }
  103. return Object.values(metrics).filter(
  104. (metric) =>
  105. ![...HIDDEN_METRICS, MetricKey.security_review_rating].includes(metric.key as MetricKey),
  106. );
  107. }, [component?.qualifier, metrics]);
  108. const handleUpdateQuery = (newQuery: Query) => {
  109. const q = serializeUrlQuery({
  110. ...parsedQuery,
  111. ...newQuery,
  112. });
  113. router.push({
  114. pathname,
  115. query: {
  116. ...q,
  117. ...getBranchLikeQuery(branchLike),
  118. id: component?.key,
  119. },
  120. });
  121. };
  122. return (
  123. component && (
  124. <ProjectActivityAppRenderer
  125. analyses={analyses}
  126. analysesLoading={isLoadingAnalyses}
  127. graphLoading={isLoadingHistory}
  128. leakPeriodDate={leakPeriodDate}
  129. initializing={isLoadingAnalyses || isLoadingHistory}
  130. measuresHistory={measuresHistory}
  131. metrics={filteredMetrics}
  132. project={component}
  133. onUpdateQuery={handleUpdateQuery}
  134. query={parsedQuery}
  135. />
  136. )
  137. );
  138. }
  139. export default function RedirectWrapper() {
  140. const { query } = useLocation();
  141. const { component } = useComponent();
  142. const router = useRouter();
  143. const filtered = React.useMemo(() => {
  144. for (const key in query) {
  145. if (key !== 'id' && query[key] !== '') {
  146. return true;
  147. }
  148. }
  149. return false;
  150. }, [query]);
  151. const { graph, customGraphs } = getActivityGraph(PROJECT_ACTIVITY_GRAPH, component?.key ?? '');
  152. const emptyCustomGraph = isCustomGraph(graph) && customGraphs.length <= 0;
  153. // if there is no filter, but there are saved preferences in the localStorage
  154. // also don't redirect to custom if there is no metrics selected for it
  155. const shouldRedirect = !filtered && graph != null && graph !== DEFAULT_GRAPH && !emptyCustomGraph;
  156. React.useEffect(() => {
  157. if (shouldRedirect) {
  158. const newQuery = { ...query, graph };
  159. if (isCustomGraph(newQuery.graph)) {
  160. router.replace({ query: { ...newQuery, custom_metrics: customGraphs.join(',') } });
  161. } else {
  162. router.replace({ query: newQuery });
  163. }
  164. }
  165. }, [shouldRedirect, router, query, graph, customGraphs]);
  166. return shouldRedirect ? null : <ProjectActivityApp />;
  167. }