import {Component, Fragment} from 'react'; import {browserHistory} from 'react-router'; import styled from '@emotion/styled'; import {Location, LocationDescriptorObject} from 'history'; import GridEditable, { COL_WIDTH_UNDEFINED, GridColumn, } from 'sentry/components/gridEditable'; import SortLink from 'sentry/components/gridEditable/sortLink'; import Link from 'sentry/components/links/link'; import Pagination from 'sentry/components/pagination'; import QuestionTooltip from 'sentry/components/questionTooltip'; import Tooltip from 'sentry/components/tooltip'; import {t, tct} from 'sentry/locale'; import {Organization, Project} from 'sentry/types'; import {defined} from 'sentry/utils'; import {trackAnalyticsEvent} from 'sentry/utils/analytics'; import DiscoverQuery, { TableData, TableDataRow, } from 'sentry/utils/discover/discoverQuery'; import EventView, {EventData, isFieldSortable} from 'sentry/utils/discover/eventView'; import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers'; import { fieldAlignment, getAggregateAlias, isSpanOperationBreakdownField, SPAN_OP_RELATIVE_BREAKDOWN_FIELD, } from 'sentry/utils/discover/fields'; import parseLinkHeader from 'sentry/utils/parseLinkHeader'; import CellAction, {Actions, updateQuery} from 'sentry/views/eventsV2/table/cellAction'; import {TableColumn} from 'sentry/views/eventsV2/table/types'; import {COLUMN_TITLES} from '../../data'; import { generateTraceLink, generateTransactionLink, normalizeSearchConditions, } from '../utils'; import OperationSort, {TitleProps} from './operationSort'; export function getProjectID( eventData: EventData, projects: Project[] ): string | undefined { const projectSlug = (eventData?.project as string) || undefined; if (typeof projectSlug === undefined) { return undefined; } const project = projects.find(currentProject => currentProject.slug === projectSlug); if (!project) { return undefined; } return project.id; } class OperationTitle extends Component { render() { const {onClick} = this.props; return (
{t('operation duration')}
); } } type Props = { eventView: EventView; location: Location; organization: Organization; setError: (msg: string | undefined) => void; totalEventCount: string; transactionName: string; columnTitles?: string[]; }; type State = { widths: number[]; }; class EventsTable extends Component { state: State = { widths: [], }; handleCellAction = (column: TableColumn) => { return (action: Actions, value: React.ReactText) => { const {eventView, location, organization} = this.props; trackAnalyticsEvent({ eventKey: 'performance_views.transactionEvents.cellaction', eventName: 'Performance Views: Transaction Events Tab Cell Action Clicked', organization_id: parseInt(organization.id, 10), action, }); const searchConditions = normalizeSearchConditions(eventView.query); updateQuery(searchConditions, action, column, value); browserHistory.push({ pathname: location.pathname, query: { ...location.query, cursor: undefined, query: searchConditions.formatString(), }, }); }; }; renderBodyCell( tableData: TableData | null, column: TableColumn, dataRow: TableDataRow ): React.ReactNode { const {eventView, organization, location, transactionName} = this.props; if (!tableData || !tableData.meta) { return dataRow[column.key]; } const tableMeta = tableData.meta; const field = String(column.key); const fieldRenderer = getFieldRenderer(field, tableMeta); const rendered = fieldRenderer(dataRow, {organization, location, eventView}); const allowActions = [ Actions.ADD, Actions.EXCLUDE, Actions.SHOW_GREATER_THAN, Actions.SHOW_LESS_THAN, ]; if (field === 'id' || field === 'trace') { const generateLink = field === 'id' ? generateTransactionLink : generateTraceLink; const target = generateLink(transactionName)(organization, dataRow, location.query); return ( {rendered} ); } const fieldName = getAggregateAlias(field); const value = dataRow[fieldName]; if (tableMeta[fieldName] === 'integer' && defined(value) && value > 999) { return ( {rendered} ); } return ( {rendered} ); } renderBodyCellWithData = (tableData: TableData | null) => { return ( column: TableColumn, dataRow: TableDataRow ): React.ReactNode => this.renderBodyCell(tableData, column, dataRow); }; onSortClick(currentSortKind?: string, currentSortField?: string) { const {organization} = this.props; trackAnalyticsEvent({ eventKey: 'performance_views.transactionEvents.sort', eventName: 'Performance Views: Transaction Events Tab Sorted', organization_id: parseInt(organization.id, 10), field: currentSortField, direction: currentSortKind, }); } renderHeadCell( tableMeta: TableData['meta'], column: TableColumn, title: React.ReactNode ): React.ReactNode { const {eventView, location} = this.props; const align = fieldAlignment(column.name, column.type, tableMeta); const field = {field: column.name, width: column.width}; function generateSortLink(): LocationDescriptorObject | undefined { if (!tableMeta) { return undefined; } const nextEventView = eventView.sortOnField(field, tableMeta); const queryStringObject = nextEventView.generateQueryStringObject(); return { ...location, query: {...location.query, sort: queryStringObject.sort}, }; } const currentSort = eventView.sortForField(field, tableMeta); // Event id and Trace id are technically sortable but we don't want to sort them here since sorting by a uuid value doesn't make sense const canSort = field.field !== 'id' && field.field !== 'trace' && field.field !== SPAN_OP_RELATIVE_BREAKDOWN_FIELD && isFieldSortable(field, tableMeta); const currentSortKind = currentSort ? currentSort.kind : undefined; const currentSortField = currentSort ? currentSort.field : undefined; if (field.field === SPAN_OP_RELATIVE_BREAKDOWN_FIELD) { title = ( ); } const sortLink = ( this.onSortClick(currentSortKind, currentSortField)} /> ); return sortLink; } renderHeadCellWithMeta = (tableMeta: TableData['meta']) => { const columnTitles = this.props.columnTitles ?? COLUMN_TITLES; return (column: TableColumn, index: number): React.ReactNode => this.renderHeadCell(tableMeta, column, columnTitles[index]); }; handleResizeColumn = (columnIndex: number, nextColumn: GridColumn) => { const widths: number[] = [...this.state.widths]; widths[columnIndex] = nextColumn.width ? Number(nextColumn.width) : COL_WIDTH_UNDEFINED; this.setState({widths}); }; render() { const {eventView, organization, location, setError, totalEventCount} = this.props; const totalTransactionsView = eventView.clone(); totalTransactionsView.sorts = []; totalTransactionsView.fields = [{field: 'count()', width: -1}]; const {widths} = this.state; const containsSpanOpsBreakdown = eventView .getColumns() .find( (col: TableColumn) => col.name === SPAN_OP_RELATIVE_BREAKDOWN_FIELD ); const columnOrder = eventView .getColumns() .filter( (col: TableColumn) => !containsSpanOpsBreakdown || !isSpanOperationBreakdownField(col.name) ) .map((col: TableColumn, i: number) => { if (typeof widths[i] === 'number') { return {...col, width: widths[i]}; } return col; }); return (
setError(error?.message)} referrer="api.performance.transaction-events" useEvents > {({pageLinks, isLoading, tableData}) => { const parsedPageLinks = parseLinkHeader(pageLinks); let currentEvent = parsedPageLinks?.next?.cursor.split(':')[1] ?? 0; if (!parsedPageLinks?.next?.results) { currentEvent = totalEventCount; } const paginationCaption = totalEventCount && currentEvent ? tct('Showing [currentEvent] of [totalEventCount] events', { currentEvent, totalEventCount, }) : undefined; return ( ); }}
); } } const StyledIconQuestion = styled(QuestionTooltip)` position: relative; top: 1px; left: 4px; `; export default EventsTable;