APIModuleView.tsx 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. import {Fragment, useRef, useState} from 'react';
  2. import {useTheme} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import {Location} from 'history';
  5. import moment from 'moment';
  6. import {CompactSelect} from 'sentry/components/compactSelect';
  7. import DatePageFilter from 'sentry/components/datePageFilter';
  8. import {t} from 'sentry/locale';
  9. import {space} from 'sentry/styles/space';
  10. import {Series} from 'sentry/types/echarts';
  11. import usePageFilters from 'sentry/utils/usePageFilters';
  12. import Chart from 'sentry/views/starfish/components/chart';
  13. import ChartPanel from 'sentry/views/starfish/components/chartPanel';
  14. import {HostDetails} from 'sentry/views/starfish/modules/APIModule/hostDetails';
  15. import {PERIOD_REGEX} from 'sentry/views/starfish/utils/dates';
  16. import {useSpansQuery} from 'sentry/views/starfish/utils/useSpansQuery';
  17. import {zeroFillSeries} from 'sentry/views/starfish/utils/zeroFillSeries';
  18. import {EndpointDataRow} from 'sentry/views/starfish/views/endpointDetails';
  19. import EndpointTable from './endpointTable';
  20. import HostTable from './hostTable';
  21. import {
  22. getEndpointDomainsEventView,
  23. getEndpointDomainsQuery,
  24. getEndpointGraphEventView,
  25. getEndpointGraphQuery,
  26. } from './queries';
  27. const HTTP_ACTION_OPTIONS = [
  28. {value: '', label: 'All'},
  29. ...['GET', 'POST', 'PUT', 'DELETE'].map(action => ({
  30. value: action,
  31. label: action,
  32. })),
  33. ];
  34. type Props = {
  35. location: Location;
  36. onSelect: (row: EndpointDataRow) => void;
  37. };
  38. export type DataRow = {
  39. count: number;
  40. description: string;
  41. domain: string;
  42. };
  43. export default function APIModuleView({location, onSelect}: Props) {
  44. const themes = useTheme();
  45. const pageFilter = usePageFilters();
  46. const [state, setState] = useState<{
  47. action: string;
  48. domain: string;
  49. transaction: string;
  50. }>({
  51. action: '',
  52. domain: '',
  53. transaction: '',
  54. });
  55. const endpointTableRef = useRef<HTMLInputElement>(null);
  56. const endpointsDomainEventView = getEndpointDomainsEventView({
  57. datetime: pageFilter.selection.datetime,
  58. });
  59. const endpointsDomainQuery = getEndpointDomainsQuery({
  60. datetime: pageFilter.selection.datetime,
  61. });
  62. const {isLoading: _isDomainsLoading, data: domains} = useSpansQuery({
  63. eventView: endpointsDomainEventView,
  64. queryString: endpointsDomainQuery,
  65. initialData: [],
  66. });
  67. const endpointsGraphEventView = getEndpointGraphEventView({
  68. datetime: pageFilter.selection.datetime,
  69. });
  70. const {isLoading: isGraphLoading, data: graphData} = useSpansQuery({
  71. eventView: endpointsGraphEventView,
  72. queryString: getEndpointGraphQuery({
  73. datetime: pageFilter.selection.datetime,
  74. }),
  75. initialData: [],
  76. });
  77. const quantiles = [
  78. 'p50(span.self_time)',
  79. 'p75(span.self_time)',
  80. 'p95(span.self_time)',
  81. 'p99(span.self_time)',
  82. ];
  83. const seriesByQuantile: {[quantile: string]: Series} = {};
  84. quantiles.forEach(quantile => {
  85. seriesByQuantile[quantile] = {
  86. seriesName: quantile,
  87. data: [],
  88. };
  89. });
  90. const countSeries: Series = {
  91. seriesName: 'count',
  92. data: [],
  93. };
  94. const failureRateSeries: Series = {
  95. seriesName: 'failure_rate',
  96. data: [],
  97. };
  98. graphData.forEach(datum => {
  99. quantiles.forEach(quantile => {
  100. seriesByQuantile[quantile].data.push({
  101. value: datum[quantile],
  102. name: datum.interval,
  103. });
  104. });
  105. countSeries.data.push({
  106. value: datum['count()'],
  107. name: datum.interval,
  108. });
  109. failureRateSeries.data.push({
  110. value: datum['failure_rate()'],
  111. name: datum.interval,
  112. });
  113. });
  114. const [_, num, unit] = pageFilter.selection.datetime.period?.match(PERIOD_REGEX) ?? [];
  115. const startTime =
  116. num && unit
  117. ? moment().subtract(num, unit as 'h' | 'd')
  118. : moment(pageFilter.selection.datetime.start);
  119. const endTime = moment(pageFilter.selection.datetime.end ?? undefined);
  120. const [zeroFilledQuantiles, zeroFilledCounts, zeroFilledFailureRate] = [
  121. seriesByQuantile,
  122. [countSeries],
  123. [failureRateSeries],
  124. ].map(seriesGroup =>
  125. Object.values(seriesGroup).map(series =>
  126. zeroFillSeries(series, moment.duration(12, 'hours'), startTime, endTime)
  127. )
  128. );
  129. const setAction = (action: string) => {
  130. setState({
  131. ...state,
  132. action,
  133. });
  134. };
  135. const setDomain = (domain: string) => {
  136. setState({
  137. ...state,
  138. domain,
  139. });
  140. };
  141. const domainOptions = [
  142. {value: '', label: 'All'},
  143. ...domains
  144. .filter(({domain}) => domain !== '')
  145. .map(({domain}) => ({
  146. value: domain,
  147. label: domain,
  148. })),
  149. ];
  150. return (
  151. <Fragment>
  152. <FilterOptionsContainer>
  153. <CompactSelect
  154. triggerProps={{prefix: t('Service')}}
  155. value="project"
  156. options={[{value: 'project', label: 'Project'}]}
  157. onChange={() => void 0}
  158. />
  159. <DatePageFilter alignDropdown="left" />
  160. </FilterOptionsContainer>
  161. <ChartsContainer>
  162. <ChartsContainerItem>
  163. <ChartPanel title={t('Throughput')}>
  164. <APIModuleChart data={zeroFilledCounts} loading={isGraphLoading} />
  165. </ChartPanel>
  166. </ChartsContainerItem>
  167. <ChartsContainerItem>
  168. <ChartPanel title={t('Response Time')}>
  169. <APIModuleChart data={zeroFilledQuantiles} loading={isGraphLoading} />
  170. </ChartPanel>
  171. </ChartsContainerItem>
  172. <ChartsContainerItem>
  173. <ChartPanel title={t('Error Rate')}>
  174. <APIModuleChart
  175. data={zeroFilledFailureRate}
  176. loading={isGraphLoading}
  177. chartColors={[themes.charts.getColorPalette(2)[2]]}
  178. />
  179. </ChartPanel>
  180. </ChartsContainerItem>
  181. </ChartsContainer>
  182. <HostTable
  183. location={location}
  184. setDomainFilter={domain => {
  185. setDomain(domain);
  186. // TODO: Cheap way to scroll to the endpoints table without waiting for async request
  187. setTimeout(() => {
  188. endpointTableRef.current?.scrollIntoView({
  189. behavior: 'smooth',
  190. inline: 'start',
  191. });
  192. }, 200);
  193. }}
  194. />
  195. <FilterOptionsContainer>
  196. <CompactSelect
  197. triggerProps={{prefix: t('Operation')}}
  198. value={state.action}
  199. options={HTTP_ACTION_OPTIONS}
  200. onChange={({value}) => setAction(value)}
  201. />
  202. <CompactSelect
  203. triggerProps={{prefix: t('Domain')}}
  204. value={state.domain}
  205. options={domainOptions}
  206. onChange={({value}) => setDomain(value)}
  207. />
  208. </FilterOptionsContainer>
  209. <div ref={endpointTableRef}>
  210. {state.domain && <HostDetails host={state.domain} />}
  211. <EndpointTable
  212. location={location}
  213. onSelect={onSelect}
  214. filterOptions={{...state, datetime: pageFilter.selection.datetime}}
  215. />
  216. </div>
  217. </Fragment>
  218. );
  219. }
  220. function APIModuleChart({
  221. data,
  222. loading,
  223. chartColors,
  224. }: {
  225. data: Series[];
  226. loading: boolean;
  227. chartColors?: string[];
  228. }) {
  229. const themes = useTheme();
  230. return (
  231. <Chart
  232. statsPeriod="24h"
  233. height={140}
  234. data={data}
  235. start=""
  236. end=""
  237. loading={loading}
  238. utc={false}
  239. grid={{
  240. left: '0',
  241. right: '0',
  242. top: '8px',
  243. bottom: '0',
  244. }}
  245. definedAxisTicks={4}
  246. stacked
  247. isLineChart
  248. chartColors={chartColors ?? themes.charts.getColorPalette(2)}
  249. disableXAxis
  250. />
  251. );
  252. }
  253. const ChartsContainer = styled('div')`
  254. display: flex;
  255. flex-direction: row;
  256. flex-wrap: wrap;
  257. gap: ${space(2)};
  258. `;
  259. const ChartsContainerItem = styled('div')`
  260. flex: 1;
  261. `;
  262. const FilterOptionsContainer = styled('div')`
  263. display: flex;
  264. flex-direction: row;
  265. gap: ${space(1)};
  266. margin-bottom: ${space(2)};
  267. `;