import {CSSProperties, Fragment} from 'react'; import {useTheme} from '@emotion/react'; import styled from '@emotion/styled'; import * as qs from 'query-string'; import GridEditable, {GridColumnHeader} from 'sentry/components/gridEditable'; import Link from 'sentry/components/links/link'; import Truncate from 'sentry/components/truncate'; import {CHART_PALETTE} from 'sentry/constants/chartPalette'; import {IconArrow} from 'sentry/icons'; import {Series} from 'sentry/types/echarts'; import {useLocation} from 'sentry/utils/useLocation'; import {MultiSparkline} from 'sentry/views/starfish/components/sparkline'; import {Sort} from 'sentry/views/starfish/modules/databaseModule'; import {DataRow} from 'sentry/views/starfish/modules/databaseModule/databaseTableView'; import {TransactionListDataRow} from 'sentry/views/starfish/modules/databaseModule/panel'; export type PanelSort = Sort; type Props = { isDataLoading: boolean; onClickSort: (sort: PanelSort) => void; row: DataRow; sort: PanelSort; spanP50Data: Series[]; spmData: Series[]; tableData: TransactionListDataRow[]; tpmData: Series[]; txnP50Data: Series[]; markLine?: Series; }; type Keys = 'transaction' | 'spm' | 'p50' | 'frequency' | 'uniqueEvents' | 'example'; type TableColumnHeader = GridColumnHeader; const COLUMN_ORDER: TableColumnHeader[] = [ { key: 'transaction', name: 'Transaction', width: 400, }, { key: 'spm', name: 'TPM v SPM', width: 200, }, { key: 'p50', name: 'Span p50 vs Txn p50', width: 200, }, ]; function QueryTransactionTable(props: Props) { const { isDataLoading, tableData, sort, onClickSort, row, spmData, tpmData, spanP50Data, txnP50Data, markLine, } = props; const location = useLocation(); const theme = useTheme(); const minMax = calculateOutlierMinMax(tableData); const onSortClick = (col: TableColumnHeader) => { let direction: 'desc' | 'asc' | undefined = undefined; if (!sort.direction || col.key !== sort.sortHeader?.key) { direction = 'desc'; } else if (sort.direction === 'desc') { direction = 'asc'; } onClickSort({direction, sortHeader: col}); }; const renderHeadCell = (col: TableColumnHeader): React.ReactNode => { const {key, name} = col; const sortableKeys: Keys[] = ['p50', 'spm']; if (sortableKeys.includes(key)) { const isBeingSorted = col.key === sort.sortHeader?.key; const direction = isBeingSorted ? sort.direction : undefined; return ( onSortClick(col)} direction={direction} title={name} /> ); } return {name}; }; const renderBodyCell = ( column: TableColumnHeader, dataRow: TransactionListDataRow ): React.ReactNode => { const {key} = column; const value = dataRow[key]; const style: CSSProperties = {}; style['min-height'] = '40px'; if ( minMax[key] && ((value as number) > minMax[key].max || (value as number) < minMax[key].min) ) { style.color = theme.red400; } const SpmSeries = spmData.length && spmData.find(item => item.seriesName === dataRow.transaction); const TpmSeries = tpmData.length && tpmData.find(item => item.seriesName === dataRow.transaction); const SP50Series = spanP50Data.length && spanP50Data.find(item => item.seriesName === dataRow.transaction); const TP50Series = txnP50Data.length && txnP50Data.find(item => item.seriesName === dataRow.transaction); if (key === 'spm' && SpmSeries && TpmSeries) { return ( ); } if (key === 'transaction') { return ( Span appears {dataRow.frequency?.toFixed(2)}x per txn ({dataRow.uniqueEvents}{' '} total txns) sample ); } if (key === 'p50' && SP50Series && TP50Series) { return ( ); } return {dataRow[key]}; }; return ( renderBodyCell(column, dataRow), }} location={location} /> ); } export function SortableHeader({title, direction, onClick}) { const arrow = !direction ? null : ( ); return ( {title} {arrow} ); } // Calculates the outlier min max for all number based rows based on the IQR Method const calculateOutlierMinMax = ( data: TransactionListDataRow[] ): Record => { const minMax: Record = {}; if (data.length > 0) { Object.entries(data[0]).forEach(([colKey, value]) => { if (typeof value === 'number') { minMax[colKey] = findOutlierMinMax(data, colKey); } }); } return minMax; }; function findOutlierMinMax(data: any[], property: string): {max: number; min: number} { const sortedValues = [...data].sort((a, b) => a[property] - b[property]); if (data.length < 4) { return {min: data[0][property], max: data[data.length - 1][property]}; } const q1 = sortedValues[Math.floor(sortedValues.length * (1 / 4))][property]; const q3 = sortedValues[Math.ceil(sortedValues.length * (3 / 4))][property]; const iqr = q3 - q1; return {min: q1 - iqr * 1.5, max: q3 + iqr * 1.5}; } const HeaderWrapper = styled('div')` cursor: pointer; `; const StyledIconArrow = styled(IconArrow)` vertical-align: top; `; export default QueryTransactionTable;