tagDistributionMeter.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. import * as React from 'react';
  2. import isPropValid from '@emotion/is-prop-valid';
  3. import styled from '@emotion/styled';
  4. import {LocationDescriptor} from 'history';
  5. import {TagSegment} from 'sentry/actionCreators/events';
  6. import Link from 'sentry/components/links/link';
  7. import Tooltip from 'sentry/components/tooltip';
  8. import Version from 'sentry/components/version';
  9. import {t} from 'sentry/locale';
  10. import overflowEllipsis from 'sentry/styles/overflowEllipsis';
  11. import space from 'sentry/styles/space';
  12. import {percent} from 'sentry/utils';
  13. type DefaultProps = {
  14. isLoading: boolean;
  15. showReleasePackage: boolean;
  16. hasError: boolean;
  17. renderLoading: () => React.ReactNode;
  18. renderEmpty: () => React.ReactNode;
  19. renderError: () => React.ReactNode;
  20. };
  21. type Props = DefaultProps & {
  22. title: string;
  23. segments: TagSegment[];
  24. totalValues: number;
  25. onTagClick?: (title: string, value: TagSegment) => void;
  26. };
  27. type SegmentValue = {
  28. to: LocationDescriptor;
  29. onClick: () => void;
  30. index: number;
  31. };
  32. export default class TagDistributionMeter extends React.Component<Props> {
  33. static defaultProps: DefaultProps = {
  34. isLoading: false,
  35. hasError: false,
  36. renderLoading: () => null,
  37. renderEmpty: () => <p>{t('No recent data.')}</p>,
  38. renderError: () => null,
  39. showReleasePackage: false,
  40. };
  41. renderTitle() {
  42. const {segments, totalValues, title, isLoading, hasError, showReleasePackage} =
  43. this.props;
  44. if (!Array.isArray(segments) || segments.length <= 0) {
  45. return (
  46. <Title>
  47. <TitleType>{title}</TitleType>
  48. </Title>
  49. );
  50. }
  51. const largestSegment = segments[0];
  52. const pct = percent(largestSegment.count, totalValues);
  53. const pctLabel = Math.floor(pct);
  54. const renderLabel = () => {
  55. switch (title) {
  56. case 'release':
  57. return (
  58. <Label>
  59. <Version
  60. version={largestSegment.name}
  61. anchor={false}
  62. tooltipRawVersion
  63. withPackage={showReleasePackage}
  64. truncate
  65. />
  66. </Label>
  67. );
  68. default:
  69. return <Label>{largestSegment.name || t('n/a')}</Label>;
  70. }
  71. };
  72. return (
  73. <Title>
  74. <TitleType>{title}</TitleType>
  75. <TitleDescription>
  76. {renderLabel()}
  77. {isLoading || hasError ? null : <Percent>{pctLabel}%</Percent>}
  78. </TitleDescription>
  79. </Title>
  80. );
  81. }
  82. renderSegments() {
  83. const {
  84. segments,
  85. onTagClick,
  86. title,
  87. isLoading,
  88. hasError,
  89. totalValues,
  90. renderLoading,
  91. renderError,
  92. renderEmpty,
  93. showReleasePackage,
  94. } = this.props;
  95. if (isLoading) {
  96. return renderLoading();
  97. }
  98. if (hasError) {
  99. return <SegmentBar>{renderError()}</SegmentBar>;
  100. }
  101. if (totalValues === 0) {
  102. return <SegmentBar>{renderEmpty()}</SegmentBar>;
  103. }
  104. return (
  105. <SegmentBar>
  106. {segments.map((value, index) => {
  107. const pct = percent(value.count, totalValues);
  108. const pctLabel = Math.floor(pct);
  109. const renderTooltipValue = () => {
  110. switch (title) {
  111. case 'release':
  112. return (
  113. <Version
  114. version={value.name}
  115. anchor={false}
  116. withPackage={showReleasePackage}
  117. />
  118. );
  119. default:
  120. return value.name || t('n/a');
  121. }
  122. };
  123. const tooltipHtml = (
  124. <React.Fragment>
  125. <div className="truncate">{renderTooltipValue()}</div>
  126. {pctLabel}%
  127. </React.Fragment>
  128. );
  129. const segmentProps: SegmentValue = {
  130. index,
  131. to: value.url,
  132. onClick: () => {
  133. if (onTagClick) {
  134. onTagClick(title, value);
  135. }
  136. },
  137. };
  138. return (
  139. <div
  140. data-test-id={`tag-${title}-segment-${value.value}`}
  141. key={value.value}
  142. style={{width: pct + '%'}}
  143. >
  144. <Tooltip title={tooltipHtml} containerDisplayMode="block">
  145. {value.isOther ? <OtherSegment /> : <Segment {...segmentProps} />}
  146. </Tooltip>
  147. </div>
  148. );
  149. })}
  150. </SegmentBar>
  151. );
  152. }
  153. render() {
  154. const {segments, totalValues} = this.props;
  155. const totalVisible = segments.reduce((sum, value) => sum + value.count, 0);
  156. const hasOther = totalVisible < totalValues;
  157. if (hasOther) {
  158. segments.push({
  159. isOther: true,
  160. name: t('Other'),
  161. value: 'other',
  162. count: totalValues - totalVisible,
  163. url: '',
  164. });
  165. }
  166. return (
  167. <TagSummary>
  168. {this.renderTitle()}
  169. {this.renderSegments()}
  170. </TagSummary>
  171. );
  172. }
  173. }
  174. const COLORS = [
  175. '#3A3387',
  176. '#5F40A3',
  177. '#8C4FBD',
  178. '#B961D3',
  179. '#DE76E4',
  180. '#EF91E8',
  181. '#F7B2EC',
  182. '#FCD8F4',
  183. '#FEEBF9',
  184. ];
  185. const TagSummary = styled('div')`
  186. margin-bottom: ${space(1)};
  187. `;
  188. const SegmentBar = styled('div')`
  189. display: flex;
  190. overflow: hidden;
  191. border-radius: 2px;
  192. `;
  193. const Title = styled('div')`
  194. display: flex;
  195. font-size: ${p => p.theme.fontSizeSmall};
  196. justify-content: space-between;
  197. `;
  198. const TitleType = styled('div')`
  199. color: ${p => p.theme.textColor};
  200. font-weight: bold;
  201. ${overflowEllipsis};
  202. `;
  203. const TitleDescription = styled('div')`
  204. display: flex;
  205. color: ${p => p.theme.gray300};
  206. text-align: right;
  207. `;
  208. const Label = styled('div')`
  209. ${overflowEllipsis};
  210. max-width: 150px;
  211. `;
  212. const Percent = styled('div')`
  213. font-weight: bold;
  214. font-variant-numeric: tabular-nums;
  215. padding-left: ${space(0.5)};
  216. color: ${p => p.theme.textColor};
  217. `;
  218. const OtherSegment = styled('span')`
  219. display: block;
  220. width: 100%;
  221. height: 16px;
  222. color: inherit;
  223. outline: none;
  224. background-color: ${COLORS[COLORS.length - 1]};
  225. `;
  226. const Segment = styled(Link, {shouldForwardProp: isPropValid})<SegmentValue>`
  227. display: block;
  228. width: 100%;
  229. height: 16px;
  230. color: inherit;
  231. outline: none;
  232. background-color: ${p => COLORS[p.index]};
  233. `;