queryTransactionTable.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. import {CSSProperties, Fragment} from 'react';
  2. import {useTheme} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import * as qs from 'query-string';
  5. import GridEditable, {GridColumnHeader} from 'sentry/components/gridEditable';
  6. import Link from 'sentry/components/links/link';
  7. import Truncate from 'sentry/components/truncate';
  8. import {CHART_PALETTE} from 'sentry/constants/chartPalette';
  9. import {IconArrow} from 'sentry/icons';
  10. import {Series} from 'sentry/types/echarts';
  11. import {useLocation} from 'sentry/utils/useLocation';
  12. import {MultiSparkline} from 'sentry/views/starfish/components/sparkline';
  13. import {Sort} from 'sentry/views/starfish/modules/databaseModule';
  14. import {DataRow} from 'sentry/views/starfish/modules/databaseModule/databaseTableView';
  15. import {TransactionListDataRow} from 'sentry/views/starfish/modules/databaseModule/panel';
  16. export type PanelSort = Sort<TableColumnHeader>;
  17. type Props = {
  18. isDataLoading: boolean;
  19. onClickSort: (sort: PanelSort) => void;
  20. row: DataRow;
  21. sort: PanelSort;
  22. spanP50Data: Series[];
  23. spmData: Series[];
  24. tableData: TransactionListDataRow[];
  25. tpmData: Series[];
  26. txnP50Data: Series[];
  27. markLine?: Series;
  28. };
  29. type Keys = 'transaction' | 'spm' | 'p50' | 'frequency' | 'uniqueEvents' | 'example';
  30. type TableColumnHeader = GridColumnHeader<Keys>;
  31. const COLUMN_ORDER: TableColumnHeader[] = [
  32. {
  33. key: 'transaction',
  34. name: 'Transaction',
  35. width: 400,
  36. },
  37. {
  38. key: 'spm',
  39. name: 'TPM v SPM',
  40. width: 200,
  41. },
  42. {
  43. key: 'p50',
  44. name: 'Span p50 vs Txn p50',
  45. width: 200,
  46. },
  47. ];
  48. function QueryTransactionTable(props: Props) {
  49. const {
  50. isDataLoading,
  51. tableData,
  52. sort,
  53. onClickSort,
  54. row,
  55. spmData,
  56. tpmData,
  57. spanP50Data,
  58. txnP50Data,
  59. markLine,
  60. } = props;
  61. const location = useLocation();
  62. const theme = useTheme();
  63. const minMax = calculateOutlierMinMax(tableData);
  64. const onSortClick = (col: TableColumnHeader) => {
  65. let direction: 'desc' | 'asc' | undefined = undefined;
  66. if (!sort.direction || col.key !== sort.sortHeader?.key) {
  67. direction = 'desc';
  68. } else if (sort.direction === 'desc') {
  69. direction = 'asc';
  70. }
  71. onClickSort({direction, sortHeader: col});
  72. };
  73. const renderHeadCell = (col: TableColumnHeader): React.ReactNode => {
  74. const {key, name} = col;
  75. const sortableKeys: Keys[] = ['p50', 'spm'];
  76. if (sortableKeys.includes(key)) {
  77. const isBeingSorted = col.key === sort.sortHeader?.key;
  78. const direction = isBeingSorted ? sort.direction : undefined;
  79. return (
  80. <SortableHeader
  81. onClick={() => onSortClick(col)}
  82. direction={direction}
  83. title={name}
  84. />
  85. );
  86. }
  87. return <span>{name}</span>;
  88. };
  89. const renderBodyCell = (
  90. column: TableColumnHeader,
  91. dataRow: TransactionListDataRow
  92. ): React.ReactNode => {
  93. const {key} = column;
  94. const value = dataRow[key];
  95. const style: CSSProperties = {};
  96. style['min-height'] = '40px';
  97. if (
  98. minMax[key] &&
  99. ((value as number) > minMax[key].max || (value as number) < minMax[key].min)
  100. ) {
  101. style.color = theme.red400;
  102. }
  103. const SpmSeries =
  104. spmData.length && spmData.find(item => item.seriesName === dataRow.transaction);
  105. const TpmSeries =
  106. tpmData.length && tpmData.find(item => item.seriesName === dataRow.transaction);
  107. const SP50Series =
  108. spanP50Data.length &&
  109. spanP50Data.find(item => item.seriesName === dataRow.transaction);
  110. const TP50Series =
  111. txnP50Data.length &&
  112. txnP50Data.find(item => item.seriesName === dataRow.transaction);
  113. if (key === 'spm' && SpmSeries && TpmSeries) {
  114. return (
  115. <MultiSparkline
  116. color={[CHART_PALETTE[4][0], CHART_PALETTE[4][3]]}
  117. series={[SpmSeries, TpmSeries]}
  118. markLine={markLine}
  119. width={column.width ? column.width - column.width / 5 : undefined}
  120. height={40}
  121. />
  122. );
  123. }
  124. if (key === 'transaction') {
  125. return (
  126. <Fragment>
  127. <Link
  128. to={`/starfish/span/${encodeURIComponent(row.group_id)}?${qs.stringify({
  129. transaction: dataRow.transaction,
  130. })}`}
  131. >
  132. <Truncate value={dataRow[column.key]} maxLength={50} />
  133. </Link>
  134. <span>
  135. Span appears {dataRow.frequency?.toFixed(2)}x per txn ({dataRow.uniqueEvents}{' '}
  136. total txns)
  137. </span>
  138. <Link to={`/performance/sentry:${dataRow.example}/`}>sample</Link>
  139. </Fragment>
  140. );
  141. }
  142. if (key === 'p50' && SP50Series && TP50Series) {
  143. return (
  144. <MultiSparkline
  145. color={[CHART_PALETTE[4][0], CHART_PALETTE[4][2]]}
  146. series={[SP50Series, TP50Series]}
  147. markLine={markLine}
  148. width={column.width ? column.width - column.width / 5 : undefined}
  149. />
  150. );
  151. }
  152. return <span style={style}>{dataRow[key]}</span>;
  153. };
  154. return (
  155. <GridEditable
  156. isLoading={isDataLoading}
  157. data={tableData}
  158. columnOrder={COLUMN_ORDER}
  159. columnSortBy={[]}
  160. grid={{
  161. renderHeadCell,
  162. renderBodyCell: (column: TableColumnHeader, dataRow: TransactionListDataRow) =>
  163. renderBodyCell(column, dataRow),
  164. }}
  165. location={location}
  166. />
  167. );
  168. }
  169. export function SortableHeader({title, direction, onClick}) {
  170. const arrow = !direction ? null : (
  171. <StyledIconArrow size="xs" direction={direction === 'desc' ? 'down' : 'up'} />
  172. );
  173. return (
  174. <HeaderWrapper onClick={onClick}>
  175. {title} {arrow}
  176. </HeaderWrapper>
  177. );
  178. }
  179. // Calculates the outlier min max for all number based rows based on the IQR Method
  180. const calculateOutlierMinMax = (
  181. data: TransactionListDataRow[]
  182. ): Record<string, {max: number; min: number}> => {
  183. const minMax: Record<string, {max: number; min: number}> = {};
  184. if (data.length > 0) {
  185. Object.entries(data[0]).forEach(([colKey, value]) => {
  186. if (typeof value === 'number') {
  187. minMax[colKey] = findOutlierMinMax(data, colKey);
  188. }
  189. });
  190. }
  191. return minMax;
  192. };
  193. function findOutlierMinMax(data: any[], property: string): {max: number; min: number} {
  194. const sortedValues = [...data].sort((a, b) => a[property] - b[property]);
  195. if (data.length < 4) {
  196. return {min: data[0][property], max: data[data.length - 1][property]};
  197. }
  198. const q1 = sortedValues[Math.floor(sortedValues.length * (1 / 4))][property];
  199. const q3 = sortedValues[Math.ceil(sortedValues.length * (3 / 4))][property];
  200. const iqr = q3 - q1;
  201. return {min: q1 - iqr * 1.5, max: q3 + iqr * 1.5};
  202. }
  203. const HeaderWrapper = styled('div')`
  204. cursor: pointer;
  205. `;
  206. const StyledIconArrow = styled(IconArrow)`
  207. vertical-align: top;
  208. `;
  209. export default QueryTransactionTable;