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.

BranchAnalysisList.tsx 4.6KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2021 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 { subDays } from 'date-fns';
  21. import { throttle } from 'lodash';
  22. import * as React from 'react';
  23. import { parseDate, toShortNotSoISOString } from 'sonar-ui-common/helpers/dates';
  24. import { scrollToElement } from 'sonar-ui-common/helpers/scrolling';
  25. import { getProjectActivity } from '../../../api/projectActivity';
  26. import BranchAnalysisListRenderer from './BranchAnalysisListRenderer';
  27. interface Props {
  28. analysis: string;
  29. branch: string;
  30. component: string;
  31. onSelectAnalysis: (analysis: T.ParsedAnalysis) => void;
  32. }
  33. interface State {
  34. analyses: T.ParsedAnalysis[];
  35. loading: boolean;
  36. range: number;
  37. scroll: number;
  38. }
  39. const STICKY_BADGE_SCROLL_OFFSET = 10;
  40. export default class BranchAnalysisList extends React.PureComponent<Props, State> {
  41. mounted = false;
  42. badges: T.Dict<HTMLDivElement> = {};
  43. scrollableNode?: HTMLDivElement;
  44. state: State = {
  45. analyses: [],
  46. loading: true,
  47. range: 30,
  48. scroll: 0
  49. };
  50. constructor(props: Props) {
  51. super(props);
  52. this.updateScroll = throttle(this.updateScroll, 20);
  53. }
  54. componentDidMount() {
  55. this.mounted = true;
  56. this.fetchAnalyses(true);
  57. }
  58. componentWillUnmount() {
  59. this.mounted = false;
  60. }
  61. scrollToSelected() {
  62. const selectedNode = document.querySelector('.branch-analysis.selected');
  63. if (this.scrollableNode && selectedNode) {
  64. scrollToElement(selectedNode, { parent: this.scrollableNode, bottomOffset: 40 });
  65. }
  66. }
  67. fetchAnalyses(initial = false) {
  68. const { analysis, branch, component } = this.props;
  69. const { range } = this.state;
  70. this.setState({ loading: true });
  71. return getProjectActivity({
  72. branch,
  73. project: component,
  74. from: range ? toShortNotSoISOString(subDays(new Date(), range)) : undefined
  75. }).then((result: { analyses: T.Analysis[] }) => {
  76. // If the selected analysis wasn't found in the default 30 days range, redo the search
  77. if (initial && analysis && !result.analyses.find(a => a.key === analysis)) {
  78. this.handleRangeChange({ value: 0 });
  79. return;
  80. }
  81. this.setState(
  82. {
  83. analyses: result.analyses.map(analysis => ({
  84. ...analysis,
  85. date: parseDate(analysis.date)
  86. })) as T.ParsedAnalysis[],
  87. loading: false
  88. },
  89. () => {
  90. this.scrollToSelected();
  91. }
  92. );
  93. });
  94. }
  95. handleScroll = (e: React.SyntheticEvent<HTMLDivElement>) => {
  96. if (e.currentTarget) {
  97. this.updateScroll(e.currentTarget.scrollTop);
  98. }
  99. };
  100. updateScroll = (scroll: number) => {
  101. this.setState({ scroll });
  102. };
  103. registerBadgeNode = (version: string) => (el: HTMLDivElement) => {
  104. if (el) {
  105. if (!el.getAttribute('originOffsetTop')) {
  106. el.setAttribute('originOffsetTop', String(el.offsetTop));
  107. }
  108. this.badges[version] = el;
  109. }
  110. };
  111. shouldStick = (version: string) => {
  112. const badge = this.badges[version];
  113. return (
  114. !!badge &&
  115. Number(badge.getAttribute('originOffsetTop')) < this.state.scroll + STICKY_BADGE_SCROLL_OFFSET
  116. );
  117. };
  118. handleRangeChange = ({ value }: { value: number }) => {
  119. this.setState({ range: value }, () => this.fetchAnalyses());
  120. };
  121. render() {
  122. const { analysis, onSelectAnalysis } = this.props;
  123. const { analyses, loading, range } = this.state;
  124. return (
  125. <BranchAnalysisListRenderer
  126. analyses={analyses}
  127. handleRangeChange={this.handleRangeChange}
  128. handleScroll={this.handleScroll}
  129. loading={loading}
  130. onSelectAnalysis={onSelectAnalysis}
  131. range={range}
  132. registerBadgeNode={this.registerBadgeNode}
  133. registerScrollableNode={el => {
  134. this.scrollableNode = el;
  135. }}
  136. selectedAnalysisKey={analysis}
  137. shouldStick={this.shouldStick}
  138. />
  139. );
  140. }
  141. }