123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303 |
- import {Fragment, useEffect, useState} from 'react';
- import styled from '@emotion/styled';
- import {Location, LocationDescriptorObject} from 'history';
- import * as qs from 'query-string';
- import GuideAnchor from 'sentry/components/assistant/guideAnchor';
- import Duration from 'sentry/components/duration';
- import GridEditable, {
- COL_WIDTH_UNDEFINED,
- GridColumn,
- } from 'sentry/components/gridEditable';
- import SortLink, {Alignments} from 'sentry/components/gridEditable/sortLink';
- import Link from 'sentry/components/links/link';
- import Pagination from 'sentry/components/pagination';
- import BaseSearchBar from 'sentry/components/searchBar';
- import {Tooltip} from 'sentry/components/tooltip';
- import {t, tct} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import {Organization, Project} from 'sentry/types';
- import DiscoverQuery, {
- TableData,
- TableDataRow,
- } from 'sentry/utils/discover/discoverQuery';
- import EventView, {isFieldSortable, MetaType} from 'sentry/utils/discover/eventView';
- import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
- import {getAggregateAlias} from 'sentry/utils/discover/fields';
- import {NumberContainer} from 'sentry/utils/discover/styles';
- import {formatPercentage} from 'sentry/utils/formatters';
- import {TableColumn} from 'sentry/views/discover/table/types';
- import ThroughputCell from 'sentry/views/starfish/components/tableCells/throughputCell';
- import {TIME_SPENT_IN_SERVICE} from 'sentry/views/starfish/utils/generatePerformanceEventView';
- import {DataTitles} from 'sentry/views/starfish/views/spans/types';
- const COLUMN_TITLES = [
- t('Endpoint'),
- DataTitles.throughput,
- t('Change'),
- DataTitles.p95,
- t('Change'),
- DataTitles.errorCount,
- t('Change'),
- DataTitles.timeSpent,
- ];
- type Props = {
- eventView: EventView;
- location: Location;
- organization: Organization;
- projects: Project[];
- setError: (msg: string | undefined) => void;
- };
- function EndpointList({eventView, location, organization, setError}: Props) {
- const [widths, setWidths] = useState<number[]>([]);
- const [_eventView, setEventView] = useState<EventView>(eventView);
- // Effect to keep the parent eventView in sync with the child, so that chart zoom and time period can be accounted for.
- useEffect(() => {
- setEventView(prevEventView => {
- const cloned = eventView.clone();
- cloned.query = prevEventView.query;
- return cloned;
- });
- }, [eventView]);
- function renderBodyCell(
- tableData: TableData | null,
- column: TableColumn<keyof TableDataRow>,
- dataRow: TableDataRow,
- _deltaColumnMap: Record<string, string>
- ): React.ReactNode {
- if (!tableData || !tableData.meta) {
- return dataRow[column.key];
- }
- const tableMeta = tableData.meta;
- const field = String(column.key);
- const fieldRenderer = getFieldRenderer(field, tableMeta, false);
- const rendered = fieldRenderer(dataRow, {organization, location});
- if (field === 'transaction') {
- let prefix = '';
- if (dataRow['http.method']) {
- prefix = `${dataRow['http.method']} `;
- }
- return (
- <Link
- to={`/organizations/${
- organization.slug
- }/starfish/endpoint-overview/?${qs.stringify({
- endpoint: dataRow.transaction,
- 'http.method': dataRow['http.method'],
- statsPeriod: eventView.statsPeriod,
- project: eventView.project,
- start: eventView.start,
- end: eventView.end,
- })}`}
- style={{display: `block`, width: `100%`}}
- >
- {prefix}
- {dataRow.transaction}
- </Link>
- );
- }
- if (field === TIME_SPENT_IN_SERVICE) {
- const cumulativeTime = Number(dataRow['sum(transaction.duration)']);
- const cumulativeTimePercentage = Number(dataRow[TIME_SPENT_IN_SERVICE]);
- return (
- <Tooltip
- title={tct('Total time spent by endpoint is [cumulativeTime])', {
- cumulativeTime: (
- <Duration seconds={cumulativeTime / 1000} fixedDigits={2} abbreviation />
- ),
- })}
- containerDisplayMode="block"
- position="top"
- >
- <NumberContainer>{formatPercentage(cumulativeTimePercentage)}</NumberContainer>
- </Tooltip>
- );
- }
- // TODO: This can be removed if/when the backend returns this field's type
- // as `"rate"` and its unit as `"1/second"
- if (field === 'tps()') {
- return <ThroughputCell throughputPerSecond={dataRow[field] as number} />;
- }
- if (field === 'project') {
- return null;
- }
- const fieldName = getAggregateAlias(field);
- const value = dataRow[fieldName];
- if (tableMeta[fieldName] === 'integer' && typeof value === 'number' && value > 999) {
- return (
- <Tooltip
- title={value.toLocaleString()}
- containerDisplayMode="block"
- position="right"
- >
- {rendered}
- </Tooltip>
- );
- }
- return rendered;
- }
- function renderBodyCellWithData(tableData: TableData | null) {
- const deltaColumnMap: Record<string, string> = {};
- if (tableData?.data?.[0]) {
- Object.keys(tableData.data[0]).forEach(col => {
- if (
- col.startsWith(
- 'equation|(percentile_range(transaction.duration,0.95,lessOrEquals'
- )
- ) {
- deltaColumnMap['p95()'] = col;
- }
- });
- }
- return (
- column: TableColumn<keyof TableDataRow>,
- dataRow: TableDataRow
- ): React.ReactNode => renderBodyCell(tableData, column, dataRow, deltaColumnMap);
- }
- function renderHeadCell(
- tableMeta: TableData['meta'],
- column: TableColumn<keyof TableDataRow>,
- title: React.ReactNode
- ): React.ReactNode {
- let align: Alignments = 'right';
- if (title === 'Endpoint') {
- align = 'left';
- }
- const field = {
- field: column.column.kind === 'equation' ? (column.key as string) : column.name,
- width: column.width,
- };
- const aggregateAliasTableMeta: MetaType = {};
- if (tableMeta) {
- Object.keys(tableMeta).forEach(key => {
- aggregateAliasTableMeta[getAggregateAlias(key)] = tableMeta[key];
- });
- }
- function generateSortLink(): LocationDescriptorObject | undefined {
- if (!tableMeta) {
- return undefined;
- }
- const nextEventView = eventView.sortOnField(field, aggregateAliasTableMeta);
- const queryStringObject = nextEventView.generateQueryStringObject();
- return {
- ...location,
- query: {...location.query, sort: queryStringObject.sort},
- };
- }
- const currentSort = eventView.sortForField(field, aggregateAliasTableMeta);
- const canSort = isFieldSortable(field, aggregateAliasTableMeta);
- const currentSortKind = currentSort ? currentSort.kind : undefined;
- const sortLink = (
- <SortLink
- align={align}
- title={title || field.field}
- direction={currentSortKind}
- canSort={canSort}
- generateSortLink={generateSortLink}
- />
- );
- return sortLink;
- }
- function renderHeadCellWithMeta(tableMeta: TableData['meta']) {
- const newColumnTitles = COLUMN_TITLES;
- return (column: TableColumn<keyof TableDataRow>, index: number): React.ReactNode =>
- renderHeadCell(tableMeta, column, newColumnTitles[index]);
- }
- function handleResizeColumn(columnIndex: number, nextColumn: GridColumn) {
- setWidths(prevWidths =>
- prevWidths.map((width, index) =>
- index === columnIndex ? Number(nextColumn.width ?? COL_WIDTH_UNDEFINED) : width
- )
- );
- }
- function handleSearch(query: string) {
- const clonedEventView = eventView.clone();
- // Default to fuzzy finding for now
- clonedEventView.query += `transaction:*${query}*`;
- setEventView(clonedEventView);
- }
- const columnOrder = eventView
- .getColumns()
- .filter(
- (col: TableColumn<React.ReactText>) =>
- col.name !== 'project' &&
- col.name !== 'http.method' &&
- col.name !== 'total.transaction_duration' &&
- col.name !== 'sum(transaction.duration)'
- )
- .map((col: TableColumn<React.ReactText>, i: number) => {
- if (typeof widths[i] === 'number') {
- return {...col, width: widths[i]};
- }
- return col;
- });
- const columnSortBy = eventView.getSorts();
- return (
- <GuideAnchor target="performance_table" position="top-start">
- <StyledSearchBar placeholder={t('Search for endpoints')} onSearch={handleSearch} />
- <DiscoverQuery
- eventView={_eventView}
- orgSlug={organization.slug}
- location={location}
- setError={error => setError(error?.message)}
- referrer="api.starfish.endpoint-list"
- queryExtras={{dataset: 'metrics'}}
- >
- {({pageLinks, isLoading, tableData}) => (
- <Fragment>
- <GridEditable
- isLoading={isLoading}
- data={tableData ? tableData.data : []}
- columnOrder={columnOrder}
- columnSortBy={columnSortBy}
- grid={{
- onResizeColumn: handleResizeColumn,
- renderHeadCell: renderHeadCellWithMeta(tableData?.meta) as any,
- renderBodyCell: renderBodyCellWithData(tableData) as any,
- }}
- location={location}
- />
- <Pagination pageLinks={pageLinks} />
- </Fragment>
- )}
- </DiscoverQuery>
- </GuideAnchor>
- );
- }
- export default EndpointList;
- const StyledSearchBar = styled(BaseSearchBar)`
- margin-bottom: ${space(2)};
- `;
|