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.

CreationDateFacet.tsx 8.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. /*
  2. * SonarQube
  3. * Copyright (C) 2009-2018 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 React from 'react';
  21. import { max } from 'lodash';
  22. import { injectIntl, InjectedIntlProps } from 'react-intl';
  23. import { Query } from '../utils';
  24. import FacetBox from '../../../components/facet/FacetBox';
  25. import FacetHeader from '../../../components/facet/FacetHeader';
  26. import FacetItem from '../../../components/facet/FacetItem';
  27. import { longFormatterOption } from '../../../components/intl/DateFormatter';
  28. import DateFromNow from '../../../components/intl/DateFromNow';
  29. import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
  30. import BarChart from '../../../components/charts/BarChart';
  31. import DateRangeInput from '../../../components/controls/DateRangeInput';
  32. import { isSameDay, parseDate } from '../../../helpers/dates';
  33. import { translate } from '../../../helpers/l10n';
  34. import { formatMeasure } from '../../../helpers/measures';
  35. import DeferredSpinner from '../../../components/common/DeferredSpinner';
  36. interface Props {
  37. component: T.Component | undefined;
  38. createdAfter: Date | undefined;
  39. createdAt: string;
  40. createdBefore: Date | undefined;
  41. createdInLast: string;
  42. fetching: boolean;
  43. onChange: (changes: Partial<Query>) => void;
  44. onToggle: (property: string) => void;
  45. open: boolean;
  46. sinceLeakPeriod: boolean;
  47. stats: { [x: string]: number } | undefined;
  48. }
  49. class CreationDateFacet extends React.PureComponent<Props & InjectedIntlProps> {
  50. property = 'createdAt';
  51. static defaultProps = {
  52. open: true
  53. };
  54. hasValue = () =>
  55. this.props.createdAfter !== undefined ||
  56. this.props.createdAt.length > 0 ||
  57. this.props.createdBefore !== undefined ||
  58. this.props.createdInLast.length > 0 ||
  59. this.props.sinceLeakPeriod;
  60. handleHeaderClick = () => {
  61. this.props.onToggle(this.property);
  62. };
  63. handleClear = () => {
  64. this.resetTo({});
  65. };
  66. resetTo = (changes: Partial<Query>) => {
  67. this.props.onChange({
  68. createdAfter: undefined,
  69. createdAt: undefined,
  70. createdBefore: undefined,
  71. createdInLast: undefined,
  72. sinceLeakPeriod: undefined,
  73. ...changes
  74. });
  75. };
  76. handleBarClick = ({
  77. createdAfter,
  78. createdBefore
  79. }: {
  80. createdAfter: Date;
  81. createdBefore?: Date;
  82. }) => {
  83. this.resetTo({ createdAfter, createdBefore });
  84. };
  85. handlePeriodChange = ({ from, to }: { from?: Date; to?: Date }) => {
  86. this.resetTo({ createdAfter: from, createdBefore: to });
  87. };
  88. handlePeriodClick = (period: string) => this.resetTo({ createdInLast: period });
  89. handleLeakPeriodClick = () => this.resetTo({ sinceLeakPeriod: true });
  90. getValues() {
  91. const { createdAfter, createdAt, createdBefore, createdInLast, sinceLeakPeriod } = this.props;
  92. const { formatDate } = this.props.intl;
  93. const values = [];
  94. if (createdAfter) {
  95. values.push(formatDate(createdAfter, longFormatterOption));
  96. }
  97. if (createdAt) {
  98. values.push(formatDate(createdAt, longFormatterOption));
  99. }
  100. if (createdBefore) {
  101. values.push(formatDate(createdBefore, longFormatterOption));
  102. }
  103. if (createdInLast === '1w') {
  104. values.push(translate('issues.facet.createdAt.last_week'));
  105. }
  106. if (createdInLast === '1m') {
  107. values.push(translate('issues.facet.createdAt.last_month'));
  108. }
  109. if (createdInLast === '1y') {
  110. values.push(translate('issues.facet.createdAt.last_year'));
  111. }
  112. if (sinceLeakPeriod) {
  113. values.push(translate('issues.new_code'));
  114. }
  115. return values;
  116. }
  117. renderBarChart() {
  118. const { createdBefore, stats } = this.props;
  119. if (!stats) {
  120. return null;
  121. }
  122. const periods = Object.keys(stats);
  123. if (periods.length < 2 || periods.every(period => !stats[period])) {
  124. return null;
  125. }
  126. const { formatDate } = this.props.intl;
  127. const data = periods.map((start, index) => {
  128. const startDate = parseDate(start);
  129. let endDate;
  130. if (index < periods.length - 1) {
  131. endDate = parseDate(periods[index + 1]);
  132. endDate.setDate(endDate.getDate() - 1);
  133. } else {
  134. endDate = createdBefore ? parseDate(createdBefore) : undefined;
  135. }
  136. const tooltipEndDate = endDate || new Date();
  137. const tooltip = (
  138. <React.Fragment>
  139. {formatMeasure(stats[start], 'SHORT_INT')}
  140. <br />
  141. {formatDate(startDate, longFormatterOption)}
  142. {!isSameDay(tooltipEndDate, startDate) &&
  143. ` - ${formatDate(tooltipEndDate, longFormatterOption)}`}
  144. </React.Fragment>
  145. );
  146. return {
  147. createdAfter: startDate,
  148. createdBefore: endDate,
  149. tooltip,
  150. x: index,
  151. y: stats[start]
  152. };
  153. });
  154. const barsWidth = Math.floor(250 / data.length);
  155. const width = barsWidth * data.length - 1 + 10;
  156. const maxValue = max(data.map(d => d.y));
  157. const xValues = data.map(d => (d.y === maxValue ? formatMeasure(maxValue, 'SHORT_INT') : ''));
  158. return (
  159. <BarChart
  160. barsWidth={barsWidth - 1}
  161. data={data}
  162. height={75}
  163. onBarClick={this.handleBarClick}
  164. padding={[25, 0, 5, 10]}
  165. width={width}
  166. xValues={xValues}
  167. />
  168. );
  169. }
  170. renderExactDate() {
  171. return (
  172. <div className="search-navigator-facet-container">
  173. <DateTimeFormatter date={this.props.createdAt} />
  174. <br />
  175. <span className="note">
  176. <DateFromNow date={this.props.createdAt} />
  177. </span>
  178. </div>
  179. );
  180. }
  181. renderPeriodSelectors() {
  182. const { createdAfter, createdBefore } = this.props;
  183. return (
  184. <div className="search-navigator-date-facet-selection">
  185. <DateRangeInput
  186. onChange={this.handlePeriodChange}
  187. value={{ from: createdAfter, to: createdBefore }}
  188. />
  189. </div>
  190. );
  191. }
  192. renderPredefinedPeriods() {
  193. const { component, createdInLast, sinceLeakPeriod } = this.props;
  194. return (
  195. <div className="spacer-top issues-predefined-periods">
  196. <FacetItem
  197. active={!this.hasValue()}
  198. name={translate('issues.facet.createdAt.all')}
  199. onClick={this.handlePeriodClick}
  200. tooltip={translate('issues.facet.createdAt.all')}
  201. value=""
  202. />
  203. {component ? (
  204. <FacetItem
  205. active={sinceLeakPeriod}
  206. name={translate('issues.new_code')}
  207. onClick={this.handleLeakPeriodClick}
  208. tooltip={translate('issues.new_code_period')}
  209. value=""
  210. />
  211. ) : (
  212. <>
  213. <FacetItem
  214. active={createdInLast === '1w'}
  215. name={translate('issues.facet.createdAt.last_week')}
  216. onClick={this.handlePeriodClick}
  217. tooltip={translate('issues.facet.createdAt.last_week')}
  218. value="1w"
  219. />
  220. <FacetItem
  221. active={createdInLast === '1m'}
  222. name={translate('issues.facet.createdAt.last_month')}
  223. onClick={this.handlePeriodClick}
  224. tooltip={translate('issues.facet.createdAt.last_month')}
  225. value="1m"
  226. />
  227. <FacetItem
  228. active={createdInLast === '1y'}
  229. name={translate('issues.facet.createdAt.last_year')}
  230. onClick={this.handlePeriodClick}
  231. tooltip={translate('issues.facet.createdAt.last_year')}
  232. value="1y"
  233. />
  234. </>
  235. )}
  236. </div>
  237. );
  238. }
  239. renderInner() {
  240. const { createdAt } = this.props;
  241. return createdAt ? (
  242. this.renderExactDate()
  243. ) : (
  244. <div>
  245. {this.renderBarChart()}
  246. {this.renderPeriodSelectors()}
  247. {this.renderPredefinedPeriods()}
  248. </div>
  249. );
  250. }
  251. render() {
  252. return (
  253. <FacetBox property={this.property}>
  254. <FacetHeader
  255. name={translate('issues.facet', this.property)}
  256. onClear={this.handleClear}
  257. onClick={this.handleHeaderClick}
  258. open={this.props.open}
  259. values={this.getValues()}
  260. />
  261. <DeferredSpinner loading={this.props.fetching} />
  262. {this.props.open && this.renderInner()}
  263. </FacetBox>
  264. );
  265. }
  266. }
  267. export default injectIntl(CreationDateFacet);