import {Fragment, useMemo} from 'react'; import styled from '@emotion/styled'; import PanelTable, {PanelTableHeader} from 'sentry/components/panels/panelTable'; import {Tooltip} from 'sentry/components/tooltip'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {MetricsQueryApiResponse} from 'sentry/types'; import {formatMetricsUsingUnitAndOp} from 'sentry/utils/metrics/formatters'; import {formatMRIField, MRIToField, parseMRI} from 'sentry/utils/metrics/mri'; import { isMetricFormula, type MetricsQueryApiQueryParams, type MetricsQueryApiRequestQuery, } from 'sentry/utils/metrics/useMetricsQuery'; import {LoadingScreen} from 'sentry/views/starfish/components/chart'; interface MetricTableContainerProps { isLoading: boolean; metricQueries: MetricsQueryApiRequestQuery[]; timeseriesData?: MetricsQueryApiResponse; } export function MetricTableContainer({ timeseriesData, metricQueries, isLoading, }: MetricTableContainerProps) { const tableData = useMemo(() => { return timeseriesData ? getTableData(timeseriesData, metricQueries) : undefined; }, [timeseriesData, metricQueries]); if (!tableData) { return null; } return ( ); } interface MetricTableProps { data: { headers: {name: string; type: string}[]; rows: any[]; }; isLoading: boolean; borderless?: boolean; } export function MetricTable({isLoading, data, borderless}: MetricTableProps) { function renderRow(row: any, index: number) { return data.headers.map((column, columnIndex) => { const key = `${index}-${columnIndex}:${column}`; const value = row[column.name]; if (!value) { return ( {column.type === 'field' ? 'n/a' : '(none)'} ); } return ( {value} ); }); } return ( { const header = formatMRIField(column.name); return ( {header} ); })} stickyHeaders isLoading={isLoading} emptyMessage={t('No results')} > {data.rows.map(renderRow)} ); } const equalGroupBys = (a: Record, b: Record) => { return JSON.stringify(a) === JSON.stringify(b); }; const getEmptyGroup = (tags: string[]) => tags.reduce((acc, tag) => { acc[tag] = ''; return acc; }, {}); function getGroupByCombos( queries: MetricsQueryApiRequestQuery[], results: MetricsQueryApiResponse['data'] ): Record[] { const groupBys = Array.from(new Set(queries.flatMap(query => query.groupBy ?? []))); const emptyBy = getEmptyGroup(groupBys); const allCombos = results.flatMap(group => { return group.map(entry => ({...emptyBy, ...entry.by})); }); const uniqueCombos = allCombos.filter( (combo, index, self) => index === self.findIndex(other => equalGroupBys(other, combo)) ); return uniqueCombos; } type Row = Record; interface TableData { headers: {name: string; type: string}[]; rows: Row[]; } export function getTableData( data: MetricsQueryApiResponse, queries: MetricsQueryApiQueryParams[] ): TableData { const filteredQueries = queries.filter( query => !isMetricFormula(query) ) as MetricsQueryApiRequestQuery[]; const fields = filteredQueries.map(query => MRIToField(query.mri, query.op)); const tags = [...new Set(filteredQueries.flatMap(query => query.groupBy ?? []))]; const normalizedResults = filteredQueries.map((query, index) => { const queryResults = data.data[index]; const meta = data.meta[index]; const lastMetaEntry = data.meta[index]?.[meta.length - 1]; const metaUnit = (lastMetaEntry && 'unit' in lastMetaEntry && lastMetaEntry.unit) || 'none'; const normalizedGroupResults = queryResults.map(group => { return { by: {...getEmptyGroup(tags), ...group.by}, totals: formatMetricsUsingUnitAndOp( group.totals, // TODO(ogi): switch to using the meta unit when it's available metaUnit ?? parseMRI(query.mri)?.unit!, query.op ), }; }); const key = MRIToField(query.mri, query.op); return {field: key, results: normalizedGroupResults}; }, {}); const groupByCombos = getGroupByCombos(filteredQueries, data.data); const rows: Row[] = groupByCombos.map(combo => { const row: Row = {...combo}; normalizedResults.forEach(({field, results}) => { const entry = results.find(e => equalGroupBys(e.by, combo)); row[field] = entry?.totals; }); return row; }); const tableData = { headers: [ ...tags.map(tagName => ({name: tagName, type: 'tag'})), ...fields.map(f => ({name: f, type: 'field'})), ], rows, }; return tableData; } const Cell = styled('div')<{type?: string}>` text-align: ${p => (p.type === 'field' ? 'right' : 'left')}; `; const StyledPanelTable = styled(PanelTable)<{borderless?: boolean}>` position: relative; display: grid; overflow: auto; margin: 0; margin-top: ${space(1.5)}; border-radius: ${p => p.theme.borderRadius}; font-size: ${p => p.theme.fontSizeMedium}; box-shadow: none; ${p => p.borderless && `border-radius: 0 0 ${p.theme.borderRadius} ${p.theme.borderRadius}; border-left: 0; border-right: 0; border-bottom: 0;`} ${PanelTableHeader} { height: min-content; } `; const HeaderCell = styled(Cell)` padding: 0 ${space(0.5)}; `; export const TableCell = styled(Cell)<{noValue?: boolean}>` padding: ${space(1)} ${space(3)}; ${p => p.noValue && `color: ${p.theme.gray300};`} `;