breakdownBar.tsx 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334
  1. import {Fragment, useState} from 'react';
  2. import isPropValid from '@emotion/is-prop-valid';
  3. import styled from '@emotion/styled';
  4. import {motion} from 'framer-motion';
  5. import {Tooltip} from 'sentry/components/tooltip';
  6. import {t} from 'sentry/locale';
  7. import {space} from 'sentry/styles/space';
  8. import {percent} from 'sentry/utils';
  9. import {useQuery} from 'sentry/utils/queryClient';
  10. import {DatabaseDurationChart} from 'sentry/views/starfish/views/webServiceView/databaseDurationChart';
  11. import {HttpBreakdownChart} from 'sentry/views/starfish/views/webServiceView/httpBreakdownChart';
  12. import {
  13. getDatabaseTimeSpent,
  14. getDbThroughput,
  15. getHttpThroughput,
  16. getOtherDomains,
  17. getTopHttpDomains,
  18. } from 'sentry/views/starfish/views/webServiceView/queries';
  19. const COLORS = ['#402A65', '#694D99', '#9A81C4', '#BBA6DF', '#EAE2F8'];
  20. const TOOLTIP_DELAY = 800;
  21. const HOST = 'http://localhost:8080';
  22. type ModuleSegment = {
  23. module: string;
  24. sum: number;
  25. };
  26. type Props = {
  27. segments: ModuleSegment[];
  28. title: string;
  29. transaction?: string;
  30. };
  31. function FacetBreakdownBar({segments, title, transaction: maybeTransaction}: Props) {
  32. const [hoveredValue, setHoveredValue] = useState<ModuleSegment | null>(null);
  33. const [currentSegment, setCurrentSegment] = useState<
  34. ModuleSegment['module'] | undefined
  35. >(segments[0]?.module);
  36. const totalValues = segments.reduce((acc, segment) => acc + segment.sum, 0);
  37. const transaction = maybeTransaction ?? '';
  38. const {isLoading: isHttpDurationDataLoading, data: httpDurationData} = useQuery({
  39. queryKey: [`topDomains${transaction}`],
  40. queryFn: () =>
  41. fetch(`${HOST}/?query=${getTopHttpDomains({transaction})}`).then(res => res.json()),
  42. retry: false,
  43. initialData: [],
  44. });
  45. const {isLoading: isOtherHttpDurationDataLoading, data: otherHttpDurationData} =
  46. useQuery({
  47. queryKey: [`otherDomains${transaction}`],
  48. queryFn: () =>
  49. fetch(`${HOST}/?query=${getOtherDomains({transaction})}`).then(res => res.json()),
  50. retry: false,
  51. initialData: [],
  52. });
  53. const {isLoading: isDbDurationLoading, data: dbDurationData} = useQuery({
  54. queryKey: [`databaseDuration${transaction}`],
  55. queryFn: () =>
  56. fetch(`${HOST}/?query=${getDatabaseTimeSpent({transaction})}`).then(res =>
  57. res.json()
  58. ),
  59. retry: false,
  60. initialData: [],
  61. });
  62. const {data: dbThroughputData} = useQuery({
  63. queryKey: [`dbThroughputData${transaction}`],
  64. queryFn: () =>
  65. fetch(`${HOST}/?query=${getDbThroughput({transaction})}`).then(res => res.json()),
  66. retry: false,
  67. initialData: [],
  68. });
  69. const {data: httpThroughputData} = useQuery({
  70. queryKey: [`httpThroughputData${transaction}`],
  71. queryFn: () =>
  72. fetch(`${HOST}/?query=${getHttpThroughput({transaction})}`).then(res => res.json()),
  73. retry: false,
  74. initialData: [],
  75. });
  76. function renderTitle() {
  77. return (
  78. <Title>
  79. <TitleType>{title}</TitleType>
  80. </Title>
  81. );
  82. }
  83. function renderSegments() {
  84. if (totalValues === 0) {
  85. return (
  86. <SegmentBar>
  87. <p>{t('No recent data.')}</p>
  88. </SegmentBar>
  89. );
  90. }
  91. return (
  92. <SegmentBar>
  93. {segments.map((value, index) => {
  94. const pct = percent(value.sum, totalValues);
  95. const pctLabel = Math.floor(pct);
  96. const segmentProps = {
  97. index,
  98. onClick: () => {
  99. setCurrentSegment(value.module);
  100. },
  101. };
  102. return (
  103. <div
  104. key={`segment-${value.module}`}
  105. style={{width: pct + '%'}}
  106. onMouseOver={() => {
  107. setHoveredValue(value);
  108. }}
  109. onMouseLeave={() => setHoveredValue(null)}
  110. >
  111. <Tooltip skipWrapper delay={TOOLTIP_DELAY} title={value.module}>
  112. <Segment
  113. aria-label={`${value.module} ${t('segment')}`}
  114. color={COLORS[index]}
  115. {...segmentProps}
  116. >
  117. {/* if the first segment is 6% or less, the label won't fit cleanly into the segment, so don't show the label */}
  118. {index === 0 && pctLabel > 6 ? `${pctLabel}%` : null}
  119. </Segment>
  120. </Tooltip>
  121. </div>
  122. );
  123. })}
  124. </SegmentBar>
  125. );
  126. }
  127. function renderLegend() {
  128. return (
  129. <LegendAnimateContainer expanded animate={{height: '100%', opacity: 1}}>
  130. <LegendContainer>
  131. {segments.map((segment, index) => {
  132. const pctLabel = Math.floor(percent(segment.sum, totalValues));
  133. const unfocus = !!hoveredValue && hoveredValue.module !== segment.module;
  134. const focus = hoveredValue?.module === segment.module;
  135. return (
  136. <li key={`segment-${segment.module}-${index}`}>
  137. <LegendRow
  138. onMouseOver={() => setHoveredValue(segment)}
  139. onMouseLeave={() => setHoveredValue(null)}
  140. onClick={() => setCurrentSegment(segment.module)}
  141. >
  142. <LegendDot color={COLORS[index]} focus={focus} />
  143. <LegendText unfocus={unfocus}>
  144. {segment.module ?? (
  145. <NotApplicableLabel>{t('n/a')}</NotApplicableLabel>
  146. )}
  147. </LegendText>
  148. {<LegendPercent>{`${pctLabel}%`}</LegendPercent>}
  149. </LegendRow>
  150. </li>
  151. );
  152. })}
  153. </LegendContainer>
  154. </LegendAnimateContainer>
  155. );
  156. }
  157. function renderChart(mod: string | undefined) {
  158. switch (mod) {
  159. case 'http':
  160. return (
  161. <HttpBreakdownChart
  162. isHttpDurationDataLoading={isHttpDurationDataLoading}
  163. isOtherHttpDurationDataLoading={isOtherHttpDurationDataLoading}
  164. httpDurationData={httpDurationData}
  165. otherHttpDurationData={otherHttpDurationData}
  166. httpThroughputData={httpThroughputData}
  167. />
  168. );
  169. case 'db':
  170. default:
  171. return (
  172. <DatabaseDurationChart
  173. isDbDurationLoading={isDbDurationLoading}
  174. dbDurationData={dbDurationData}
  175. dbThroughputData={dbThroughputData}
  176. />
  177. );
  178. }
  179. }
  180. return (
  181. <Fragment>
  182. <TagSummary>
  183. <details open aria-expanded onClick={e => e.preventDefault()}>
  184. <StyledSummary>
  185. <TagHeader>
  186. {renderTitle()}
  187. {renderSegments()}
  188. </TagHeader>
  189. </StyledSummary>
  190. {renderLegend()}
  191. </details>
  192. </TagSummary>
  193. {renderChart(currentSegment)}
  194. </Fragment>
  195. );
  196. }
  197. export default FacetBreakdownBar;
  198. const TagSummary = styled('div')`
  199. margin-bottom: ${space(2)};
  200. `;
  201. const TagHeader = styled('span')<{clickable?: boolean}>`
  202. ${p => (p.clickable ? 'cursor: pointer' : null)};
  203. `;
  204. const SegmentBar = styled('div')`
  205. display: flex;
  206. overflow: hidden;
  207. `;
  208. const Title = styled('div')`
  209. display: flex;
  210. font-size: ${p => p.theme.fontSizeMedium};
  211. justify-content: space-between;
  212. margin-bottom: ${space(1)};
  213. line-height: 1.1;
  214. `;
  215. const TitleType = styled('div')`
  216. flex: none;
  217. color: ${p => p.theme.textColor};
  218. font-weight: bold;
  219. font-size: ${p => p.theme.fontSizeMedium};
  220. margin-right: ${space(1)};
  221. align-self: center;
  222. `;
  223. const Segment = styled('span', {shouldForwardProp: isPropValid})<{color: string}>`
  224. &:hover {
  225. color: ${p => p.theme.white};
  226. }
  227. display: block;
  228. width: 100%;
  229. height: ${space(2)};
  230. color: ${p => p.theme.white};
  231. outline: none;
  232. background-color: ${p => p.color};
  233. text-align: right;
  234. font-size: ${p => p.theme.fontSizeExtraSmall};
  235. padding: 1px ${space(0.5)} 0 0;
  236. `;
  237. const LegendAnimateContainer = styled(motion.div, {
  238. shouldForwardProp: prop =>
  239. prop === 'animate' || (prop !== 'expanded' && isPropValid(prop)),
  240. })<{expanded: boolean}>`
  241. height: 0;
  242. opacity: 0;
  243. ${p => (!p.expanded ? 'overflow: hidden;' : '')}
  244. `;
  245. const LegendContainer = styled('ol')`
  246. list-style: none;
  247. padding: 0;
  248. margin: ${space(1)} 0;
  249. `;
  250. const LegendRow = styled('div')`
  251. display: flex;
  252. align-items: center;
  253. cursor: pointer;
  254. padding: ${space(0.5)} 0;
  255. `;
  256. const LegendDot = styled('span')<{color: string; focus: boolean}>`
  257. padding: 0;
  258. position: relative;
  259. width: 11px;
  260. height: 11px;
  261. text-indent: -9999em;
  262. display: inline-block;
  263. border-radius: 50%;
  264. flex-shrink: 0;
  265. background-color: ${p => p.color};
  266. &:after {
  267. content: '';
  268. border-radius: 50%;
  269. position: absolute;
  270. top: 0;
  271. left: 0;
  272. width: 100%;
  273. height: 100%;
  274. outline: ${p => p.theme.gray100} ${space(0.5)} solid;
  275. opacity: ${p => (p.focus ? '1' : '0')};
  276. transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1);
  277. }
  278. `;
  279. const LegendText = styled('span')<{unfocus: boolean}>`
  280. font-size: ${p => p.theme.fontSizeSmall};
  281. margin-left: ${space(1)};
  282. overflow: hidden;
  283. white-space: nowrap;
  284. text-overflow: ellipsis;
  285. transition: color 0.3s;
  286. color: ${p => (p.unfocus ? p.theme.gray300 : p.theme.gray400)};
  287. `;
  288. const LegendPercent = styled('span')`
  289. font-size: ${p => p.theme.fontSizeSmall};
  290. margin-left: ${space(1)};
  291. color: ${p => p.theme.gray300};
  292. text-align: right;
  293. flex-grow: 1;
  294. `;
  295. const NotApplicableLabel = styled('span')`
  296. color: ${p => p.theme.gray300};
  297. `;
  298. const StyledSummary = styled('summary')`
  299. &::-webkit-details-marker {
  300. display: none;
  301. }
  302. `;