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.

TabsPanel.tsx 6.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  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 { Spinner } from '@sonarsource/echoes-react';
  21. import { isBefore, sub } from 'date-fns';
  22. import { ButtonLink, FlagMessage, LightLabel, Tabs } from 'design-system';
  23. import * as React from 'react';
  24. import { FormattedMessage } from 'react-intl';
  25. import DocumentationLink from '../../../components/common/DocumentationLink';
  26. import { translate } from '../../../helpers/l10n';
  27. import { isDiffMetric } from '../../../helpers/measures';
  28. import { CodeScope } from '../../../helpers/urls';
  29. import { ApplicationPeriod } from '../../../types/application';
  30. import { ComponentQualifier } from '../../../types/component';
  31. import { Analysis, ProjectAnalysisEventCategory } from '../../../types/project-activity';
  32. import { QualityGateStatus } from '../../../types/quality-gates';
  33. import { Component, Period } from '../../../types/types';
  34. import { MAX_ANALYSES_NB } from './ActivityPanel';
  35. import { LeakPeriodInfo } from './LeakPeriodInfo';
  36. export interface MeasuresPanelProps {
  37. analyses?: Analysis[];
  38. appLeak?: ApplicationPeriod;
  39. component: Component;
  40. loading?: boolean;
  41. period?: Period;
  42. qgStatuses?: QualityGateStatus[];
  43. isNewCode: boolean;
  44. onTabSelect: (tab: CodeScope) => void;
  45. }
  46. const SQ_UPGRADE_NOTIFICATION_TIMEOUT = { weeks: 3 };
  47. export function TabsPanel(props: React.PropsWithChildren<MeasuresPanelProps>) {
  48. const {
  49. analyses,
  50. appLeak,
  51. component,
  52. loading,
  53. period,
  54. qgStatuses = [],
  55. isNewCode,
  56. children,
  57. } = props;
  58. const isApp = component.qualifier === ComponentQualifier.Application;
  59. const leakPeriod = isApp ? appLeak : period;
  60. const { failingConditionsOnNewCode, failingConditionsOnOverallCode } =
  61. countFailingConditions(qgStatuses);
  62. const recentSqUpgrade = React.useMemo(() => {
  63. if (!analyses || analyses.length === 0) {
  64. return undefined;
  65. }
  66. const notificationExpirationTime = sub(new Date(), SQ_UPGRADE_NOTIFICATION_TIMEOUT);
  67. for (const analysis of analyses.slice(0, MAX_ANALYSES_NB)) {
  68. if (isBefore(new Date(analysis.date), notificationExpirationTime)) {
  69. return undefined;
  70. }
  71. let sqUpgradeEvent = undefined;
  72. let hasQpUpdateEvent = false;
  73. for (const event of analysis.events) {
  74. sqUpgradeEvent =
  75. event.category === ProjectAnalysisEventCategory.SqUpgrade ? event : sqUpgradeEvent;
  76. hasQpUpdateEvent =
  77. hasQpUpdateEvent || event.category === ProjectAnalysisEventCategory.QualityProfile;
  78. if (sqUpgradeEvent !== undefined && hasQpUpdateEvent) {
  79. return { analysis, event: sqUpgradeEvent };
  80. }
  81. }
  82. }
  83. return undefined;
  84. }, [analyses]);
  85. const scrollToLatestSqUpgradeEvent = () => {
  86. if (recentSqUpgrade) {
  87. document
  88. .querySelector(`[data-analysis-key="${recentSqUpgrade.analysis.key}"]`)
  89. ?.scrollIntoView({
  90. behavior: 'smooth',
  91. block: 'center',
  92. inline: 'center',
  93. });
  94. }
  95. };
  96. const tabs = [
  97. {
  98. value: CodeScope.New,
  99. label: translate('overview.new_code'),
  100. counter: failingConditionsOnNewCode,
  101. },
  102. {
  103. value: CodeScope.Overall,
  104. label: translate('overview.overall_code'),
  105. counter: failingConditionsOnOverallCode,
  106. },
  107. ];
  108. return (
  109. <div className="sw-mt-3" data-testid="overview__measures-panel">
  110. {loading ? (
  111. <div>
  112. <Spinner isLoading={loading} />
  113. </div>
  114. ) : (
  115. <>
  116. {recentSqUpgrade && (
  117. <div>
  118. <FlagMessage className="sw-mb-4" variant="info">
  119. <FormattedMessage
  120. id="overview.quality_profiles_update_after_sq_upgrade.message"
  121. tagName="span"
  122. values={{
  123. link: (
  124. <ButtonLink onClick={scrollToLatestSqUpgradeEvent}>
  125. <FormattedMessage id="overview.quality_profiles_update_after_sq_upgrade.link" />
  126. </ButtonLink>
  127. ),
  128. sqVersion: recentSqUpgrade.event.name,
  129. }}
  130. />
  131. </FlagMessage>
  132. </div>
  133. )}
  134. <div className="sw-flex sw-items-center sw--mx-6">
  135. <Tabs
  136. onChange={props.onTabSelect}
  137. options={tabs}
  138. value={isNewCode ? CodeScope.New : CodeScope.Overall}
  139. >
  140. {isNewCode && leakPeriod && (
  141. <LightLabel className="sw-body-sm sw-flex sw-items-center sw-mr-6">
  142. <span className="sw-mr-1">{translate('overview.new_code')}:</span>
  143. <LeakPeriodInfo leakPeriod={leakPeriod} />
  144. </LightLabel>
  145. )}
  146. </Tabs>
  147. </div>
  148. {component.qualifier === ComponentQualifier.Application && component.needIssueSync && (
  149. <FlagMessage className="sw-mt-4" variant="info">
  150. <span>
  151. {`${translate('indexation.in_progress')} ${translate(
  152. 'indexation.details_unavailable',
  153. )}`}
  154. <DocumentationLink
  155. className="sw-ml-1 sw-whitespace-nowrap"
  156. to="/instance-administration/reindexing/"
  157. >
  158. {translate('learn_more')}
  159. </DocumentationLink>
  160. </span>
  161. </FlagMessage>
  162. )}
  163. {children}
  164. </>
  165. )}
  166. </div>
  167. );
  168. }
  169. export default React.memo(TabsPanel);
  170. function countFailingConditions(qgStatuses: QualityGateStatus[]) {
  171. let failingConditionsOnNewCode = 0;
  172. let failingConditionsOnOverallCode = 0;
  173. qgStatuses.forEach(({ failedConditions }) => {
  174. failedConditions.forEach((condition) => {
  175. if (isDiffMetric(condition.metric)) {
  176. failingConditionsOnNewCode += 1;
  177. } else {
  178. failingConditionsOnOverallCode += 1;
  179. }
  180. });
  181. });
  182. return { failingConditionsOnNewCode, failingConditionsOnOverallCode };
  183. }