Files
2024-02-15 17:01:50 +00:00

229 lines
6.0 KiB
TypeScript

import {useCallback, useMemo} from 'react';
import styled from '@emotion/styled';
import Count from 'sentry/components/count';
import type {GridColumnOrder} from 'sentry/components/gridEditable';
import GridEditable, {COL_WIDTH_UNDEFINED} from 'sentry/components/gridEditable';
import PerformanceDuration from 'sentry/components/performanceDuration';
import {ArrayLinks} from 'sentry/components/profiling/arrayLinks';
import {t} from 'sentry/locale';
import type {Project} from 'sentry/types';
import {trackAnalytics} from 'sentry/utils/analytics';
import {Container, NumberContainer} from 'sentry/utils/discover/styles';
import {getShortEventId} from 'sentry/utils/events';
import type {EventsResults, Sort} from 'sentry/utils/profiling/hooks/types';
import {generateProfileFlamechartRouteWithQuery} from 'sentry/utils/profiling/routes';
import {renderTableHead} from 'sentry/utils/profiling/tableRenderer';
import {useLocation} from 'sentry/utils/useLocation';
import useOrganization from 'sentry/utils/useOrganization';
interface FunctionsTableProps {
analyticsPageSource: 'performance_transaction' | 'profiling_transaction';
error: string | null;
functions: EventsResults<TableColumnKey>['data'];
isLoading: boolean;
project: Project | undefined;
sort: Sort<any>;
}
function FunctionsTable(props: FunctionsTableProps) {
const location = useLocation();
const organization = useOrganization();
const functions: TableDataRow[] = useMemo(() => {
const project = props.project;
if (!project) {
return [];
}
return props.functions.map(func => {
const profileIds = (func['examples()'] as unknown as string[]) || [];
return {
...func,
'examples()': profileIds.map(profileId => {
return {
value: getShortEventId(profileId),
onClick: () =>
trackAnalytics('profiling_views.go_to_flamegraph', {
organization,
source: `${props.analyticsPageSource}.suspect_functions_table`,
}),
target: generateProfileFlamechartRouteWithQuery({
orgSlug: organization.slug,
projectSlug: project.slug,
profileId,
query: {
// specify the frame to focus, the flamegraph will switch
// to the appropriate thread when these are specified
frameName: func.function as string,
framePackage: func.package as string,
},
}),
};
}),
};
});
}, [organization, props.project, props.functions, props.analyticsPageSource]);
const generateSortLink = useCallback(
(column: TableColumnKey) => {
if (!SORTABLE_COLUMNS.has(column)) {
return () => undefined;
}
const direction =
props.sort.key !== column ? 'desc' : props.sort.order === 'desc' ? 'asc' : 'desc';
return () => ({
...location,
query: {
...location.query,
functionsSort: `${direction === 'desc' ? '-' : ''}${column}`,
},
});
},
[location, props.sort]
);
return (
<GridEditable
isLoading={props.isLoading}
error={props.error}
data={functions}
columnOrder={COLUMN_ORDER.map(key => COLUMNS[key])}
columnSortBy={[]}
grid={{
renderHeadCell: renderTableHead({
currentSort: props.sort,
rightAlignedColumns: RIGHT_ALIGNED_COLUMNS,
sortableColumns: SORTABLE_COLUMNS,
generateSortLink,
}),
renderBodyCell: renderFunctionsTableCell,
}}
location={location}
/>
);
}
const RIGHT_ALIGNED_COLUMNS = new Set<TableColumnKey>(['p75()', 'sum()', 'count()']);
const SORTABLE_COLUMNS = RIGHT_ALIGNED_COLUMNS;
function renderFunctionsTableCell(
column: TableColumn,
dataRow: TableDataRow,
rowIndex: number,
columnIndex: number
) {
return (
<ProfilingFunctionsTableCell
column={column}
dataRow={dataRow}
rowIndex={rowIndex}
columnIndex={columnIndex}
/>
);
}
interface ProfilingFunctionsTableCellProps {
column: TableColumn;
columnIndex: number;
dataRow: TableDataRow;
rowIndex: number;
}
const EmptyValueContainer = styled('span')`
color: ${p => p.theme.gray300};
`;
function ProfilingFunctionsTableCell({
column,
dataRow,
}: ProfilingFunctionsTableCellProps) {
const value = dataRow[column.key];
switch (column.key) {
case 'count()':
return (
<NumberContainer>
<Count value={value} />
</NumberContainer>
);
case 'p75()':
case 'sum()':
return (
<NumberContainer>
<PerformanceDuration nanoseconds={value} abbreviation />
</NumberContainer>
);
case 'examples()':
return <ArrayLinks items={value} />;
case 'function':
case 'package':
const name = value || <EmptyValueContainer>{t('Unknown')}</EmptyValueContainer>;
return <Container>{name}</Container>;
default:
return <Container>{value}</Container>;
}
}
const FIELDS = [
'function',
'package',
'count()',
'p75()',
'sum()',
'examples()',
] as const;
type TableColumnKey = (typeof FIELDS)[number];
type TableDataRow = Record<TableColumnKey, any>;
type TableColumn = GridColumnOrder<TableColumnKey>;
const COLUMN_ORDER: TableColumnKey[] = [
'function',
'package',
'count()',
'p75()',
'sum()',
'examples()',
];
const COLUMNS: Record<TableColumnKey, TableColumn> = {
function: {
key: 'function',
name: t('Name'),
width: COL_WIDTH_UNDEFINED,
},
package: {
key: 'package',
name: t('Package'),
width: COL_WIDTH_UNDEFINED,
},
'p75()': {
key: 'p75()',
name: t('P75 Self Time'),
width: COL_WIDTH_UNDEFINED,
},
'sum()': {
key: 'sum()',
name: t('Total Self Time'),
width: COL_WIDTH_UNDEFINED,
},
'count()': {
key: 'count()',
name: t('Occurrences'),
width: COL_WIDTH_UNDEFINED,
},
'examples()': {
key: 'examples()',
name: t('Example Profiles'),
width: COL_WIDTH_UNDEFINED,
},
};
export {FunctionsTable};