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.

App.tsx 9.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310
  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 * as classNames from 'classnames';
  21. import { debounce } from 'lodash';
  22. import * as React from 'react';
  23. import AlertSuccessIcon from 'sonar-ui-common/components/icons/AlertSuccessIcon';
  24. import DeferredSpinner from 'sonar-ui-common/components/ui/DeferredSpinner';
  25. import { translate } from 'sonar-ui-common/helpers/l10n';
  26. import { getNewCodePeriod, resetNewCodePeriod, setNewCodePeriod } from '../../../api/newCodePeriod';
  27. import Suggestions from '../../../app/components/embed-docs-modal/Suggestions';
  28. import { isBranch, sortBranches } from '../../../helpers/branch-like';
  29. import { Branch, BranchLike } from '../../../types/branch-like';
  30. import '../styles.css';
  31. import { getSettingValue } from '../utils';
  32. import AppHeader from './AppHeader';
  33. import BranchList from './BranchList';
  34. import ProjectBaselineSelector from './ProjectBaselineSelector';
  35. interface Props {
  36. branchLikes: BranchLike[];
  37. branchesEnabled?: boolean;
  38. canAdmin?: boolean;
  39. component: T.Component;
  40. }
  41. interface State {
  42. analysis?: string;
  43. branchList: Branch[];
  44. currentSetting?: T.NewCodePeriodSettingType;
  45. currentSettingValue?: string;
  46. days: string;
  47. generalSetting?: T.NewCodePeriod;
  48. loading: boolean;
  49. overrideGeneralSetting?: boolean;
  50. referenceBranch?: string;
  51. saving: boolean;
  52. selected?: T.NewCodePeriodSettingType;
  53. success?: boolean;
  54. }
  55. const DEFAULT_NUMBER_OF_DAYS = '30';
  56. const DEFAULT_GENERAL_SETTING: { type: T.NewCodePeriodSettingType } = {
  57. type: 'PREVIOUS_VERSION'
  58. };
  59. export default class App extends React.PureComponent<Props, State> {
  60. mounted = false;
  61. state: State = {
  62. branchList: [],
  63. days: DEFAULT_NUMBER_OF_DAYS,
  64. loading: true,
  65. saving: false
  66. };
  67. // We use debounce as we could have multiple save in less that 3sec.
  68. resetSuccess = debounce(() => this.setState({ success: undefined }), 3000);
  69. componentDidMount() {
  70. this.mounted = true;
  71. this.fetchLeakPeriodSetting();
  72. this.sortAndFilterBranches(this.props.branchLikes);
  73. }
  74. componentDidUpdate(prevProps: Props) {
  75. if (prevProps.branchLikes !== this.props.branchLikes) {
  76. this.sortAndFilterBranches(this.props.branchLikes);
  77. }
  78. }
  79. componentWillUnmount() {
  80. this.mounted = false;
  81. }
  82. getUpdatedState(params: {
  83. currentSetting?: T.NewCodePeriodSettingType;
  84. currentSettingValue?: string;
  85. generalSetting: T.NewCodePeriod;
  86. }) {
  87. const { currentSetting, currentSettingValue, generalSetting } = params;
  88. const { referenceBranch } = this.state;
  89. const defaultDays =
  90. (generalSetting.type === 'NUMBER_OF_DAYS' && generalSetting.value) || DEFAULT_NUMBER_OF_DAYS;
  91. return {
  92. loading: false,
  93. currentSetting,
  94. currentSettingValue,
  95. generalSetting,
  96. selected: currentSetting || generalSetting.type,
  97. overrideGeneralSetting: Boolean(currentSetting),
  98. days: (currentSetting === 'NUMBER_OF_DAYS' && currentSettingValue) || defaultDays,
  99. analysis: (currentSetting === 'SPECIFIC_ANALYSIS' && currentSettingValue) || '',
  100. referenceBranch:
  101. (currentSetting === 'REFERENCE_BRANCH' && currentSettingValue) || referenceBranch
  102. };
  103. }
  104. sortAndFilterBranches(branchLikes: BranchLike[] = []) {
  105. const branchList = sortBranches(branchLikes.filter(isBranch));
  106. this.setState({ branchList, referenceBranch: branchList[0].name });
  107. }
  108. fetchLeakPeriodSetting() {
  109. this.setState({ loading: true });
  110. Promise.all([
  111. getNewCodePeriod(),
  112. getNewCodePeriod({
  113. branch: !this.props.branchesEnabled ? 'master' : undefined,
  114. project: this.props.component.key
  115. })
  116. ]).then(
  117. ([generalSetting, setting]) => {
  118. if (this.mounted) {
  119. if (!generalSetting.type) {
  120. generalSetting = DEFAULT_GENERAL_SETTING;
  121. }
  122. const currentSettingValue = setting.value;
  123. const currentSetting = setting.inherited ? undefined : setting.type || 'PREVIOUS_VERSION';
  124. this.setState(
  125. this.getUpdatedState({
  126. generalSetting,
  127. currentSetting,
  128. currentSettingValue
  129. })
  130. );
  131. }
  132. },
  133. () => {
  134. this.setState({ loading: false });
  135. }
  136. );
  137. }
  138. resetSetting = () => {
  139. this.setState({ saving: true });
  140. resetNewCodePeriod({ project: this.props.component.key }).then(
  141. () => {
  142. this.setState({
  143. saving: false,
  144. currentSetting: undefined,
  145. selected: undefined,
  146. success: true
  147. });
  148. this.resetSuccess();
  149. },
  150. () => {
  151. this.setState({ saving: false });
  152. }
  153. );
  154. };
  155. handleSelectAnalysis = (analysis: T.ParsedAnalysis) => this.setState({ analysis: analysis.key });
  156. handleSelectDays = (days: string) => this.setState({ days });
  157. handleSelectReferenceBranch = (referenceBranch: string) => {
  158. this.setState({ referenceBranch });
  159. };
  160. handleCancel = () =>
  161. this.setState(
  162. ({ generalSetting = DEFAULT_GENERAL_SETTING, currentSetting, currentSettingValue }) =>
  163. this.getUpdatedState({ generalSetting, currentSetting, currentSettingValue })
  164. );
  165. handleSelectSetting = (selected?: T.NewCodePeriodSettingType) => this.setState({ selected });
  166. handleToggleSpecificSetting = (overrideGeneralSetting: boolean) =>
  167. this.setState({ overrideGeneralSetting });
  168. handleSubmit = (e: React.SyntheticEvent<HTMLFormElement>) => {
  169. e.preventDefault();
  170. const { component } = this.props;
  171. const { analysis, days, selected: type, referenceBranch, overrideGeneralSetting } = this.state;
  172. if (!overrideGeneralSetting) {
  173. this.resetSetting();
  174. return;
  175. }
  176. const value = getSettingValue({ type, analysis, days, referenceBranch });
  177. if (type) {
  178. this.setState({ saving: true });
  179. setNewCodePeriod({
  180. project: component.key,
  181. type,
  182. value
  183. }).then(
  184. () => {
  185. this.setState({
  186. saving: false,
  187. currentSetting: type,
  188. currentSettingValue: value || undefined,
  189. success: true
  190. });
  191. this.resetSuccess();
  192. },
  193. () => {
  194. this.setState({ saving: false });
  195. }
  196. );
  197. }
  198. };
  199. render() {
  200. const { branchesEnabled, canAdmin, component } = this.props;
  201. const {
  202. analysis,
  203. branchList,
  204. currentSetting,
  205. days,
  206. generalSetting,
  207. loading,
  208. currentSettingValue,
  209. overrideGeneralSetting,
  210. referenceBranch,
  211. saving,
  212. selected,
  213. success
  214. } = this.state;
  215. return (
  216. <>
  217. <Suggestions suggestions="project_baseline" />
  218. <div className="page page-limited">
  219. <AppHeader canAdmin={!!canAdmin} />
  220. {loading ? (
  221. <DeferredSpinner />
  222. ) : (
  223. <div className="panel-white project-baseline">
  224. {branchesEnabled && <h2>{translate('project_baseline.default_setting')}</h2>}
  225. {generalSetting && overrideGeneralSetting !== undefined && (
  226. <ProjectBaselineSelector
  227. analysis={analysis}
  228. branchList={branchList}
  229. branchesEnabled={branchesEnabled}
  230. component={component.key}
  231. currentSetting={currentSetting}
  232. currentSettingValue={currentSettingValue}
  233. days={days}
  234. generalSetting={generalSetting}
  235. onCancel={this.handleCancel}
  236. onSelectAnalysis={this.handleSelectAnalysis}
  237. onSelectDays={this.handleSelectDays}
  238. onSelectReferenceBranch={this.handleSelectReferenceBranch}
  239. onSelectSetting={this.handleSelectSetting}
  240. onSubmit={this.handleSubmit}
  241. onToggleSpecificSetting={this.handleToggleSpecificSetting}
  242. overrideGeneralSetting={overrideGeneralSetting}
  243. referenceBranch={referenceBranch}
  244. saving={saving}
  245. selected={selected}
  246. />
  247. )}
  248. <div className={classNames('spacer-top', { invisible: saving || !success })}>
  249. <span className="text-success">
  250. <AlertSuccessIcon className="spacer-right" />
  251. {translate('settings.state.saved')}
  252. </span>
  253. </div>
  254. {generalSetting && branchesEnabled && (
  255. <div className="huge-spacer-top branch-baseline-selector">
  256. <hr />
  257. <h2>{translate('project_baseline.configure_branches')}</h2>
  258. <BranchList
  259. branchList={branchList}
  260. component={component}
  261. inheritedSetting={
  262. currentSetting
  263. ? {
  264. type: currentSetting,
  265. value: currentSettingValue
  266. }
  267. : generalSetting
  268. }
  269. />
  270. </div>
  271. )}
  272. </div>
  273. )}
  274. </div>
  275. </>
  276. );
  277. }
  278. }