123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466 |
- import React, {Component, Fragment, useContext, useEffect} from 'react';
- import {browserHistory} from 'react-router';
- import styled from '@emotion/styled';
- import {Location, LocationDescriptor, Query} from 'history';
- import GuideAnchor from 'sentry/components/assistant/guideAnchor';
- import {Button} from 'sentry/components/button';
- import {CompactSelect} from 'sentry/components/compactSelect';
- import DiscoverButton from 'sentry/components/discoverButton';
- import Pagination, {CursorHandler} from 'sentry/components/pagination';
- import {t} from 'sentry/locale';
- import {space} from 'sentry/styles/space';
- import {Organization} from 'sentry/types';
- import DiscoverQuery, {TableDataRow} from 'sentry/utils/discover/discoverQuery';
- import EventView from 'sentry/utils/discover/eventView';
- import {Sort} from 'sentry/utils/discover/fields';
- import {TrendsEventsDiscoverQuery} from 'sentry/utils/performance/trends/trendsDiscoverQuery';
- import {decodeScalar} from 'sentry/utils/queryString';
- import {MutableSearch} from 'sentry/utils/tokenizeSearch';
- import {Actions} from 'sentry/views/discover/table/cellAction';
- import {TableColumn} from 'sentry/views/discover/table/types';
- import {decodeColumnOrder} from 'sentry/views/discover/utils';
- import {SpanOperationBreakdownFilter} from 'sentry/views/performance/transactionSummary/filter';
- import {mapShowTransactionToPercentile} from 'sentry/views/performance/transactionSummary/transactionEvents/utils';
- import {PerformanceAtScaleContext} from 'sentry/views/performance/transactionSummary/transactionOverview/performanceAtScaleContext';
- import {
- DisplayModes,
- TransactionFilterOptions,
- } from 'sentry/views/performance/transactionSummary/utils';
- import {TrendChangeType, TrendView} from 'sentry/views/performance/trends/types';
- import TransactionsTable from './transactionsTable';
- const DEFAULT_TRANSACTION_LIMIT = 5;
- export type DropdownOption = {
- /**
- * The label to display in the dropdown
- */
- label: string;
- /**
- * The sort to apply to the eventView when this is selected.
- */
- sort: Sort;
- /**
- * The unique name to use for this option.
- */
- value: string;
- /**
- * override the eventView query
- */
- query?: [string, string][];
- /**
- * Included if the option is for a trend
- */
- trendType?: TrendChangeType;
- };
- type Props = {
- /**
- * The name of the url parameter that contains the cursor info.
- */
- cursorName: string;
- eventView: EventView;
- /**
- * The callback for when the dropdown option changes.
- */
- handleDropdownChange: (k: string) => void;
- /**
- * The limit to the number of results to fetch.
- */
- limit: number;
- location: Location;
- /**
- * The available options for the dropdown.
- */
- options: DropdownOption[];
- organization: Organization;
- /**
- * The currently selected option on the dropdown.
- */
- selected: DropdownOption;
- breakdown?: SpanOperationBreakdownFilter;
- /**
- * Show a loading indicator instead of the table, used for transaction summary p95.
- */
- forceLoading?: boolean;
- /**
- * Optional callback function to generate an alternative EventView object to be used
- * for generating the Discover query.
- */
- generateDiscoverEventView?: () => EventView;
- /**
- * A map of callbacks to generate a link for a column based on the title.
- */
- generateLink?: Record<
- string,
- (
- organization: Organization,
- tableRow: TableDataRow,
- query: Query
- ) => LocationDescriptor
- >;
- generatePerformanceTransactionEventsView?: () => EventView;
- /**
- * The callback to generate a cell action handler for a column
- */
- handleCellAction?: (
- c: TableColumn<React.ReactText>
- ) => (a: Actions, v: React.ReactText) => void;
- /**
- * The callback for when View All Events is clicked.
- */
- handleOpenAllEventsClick?: (e: React.MouseEvent<Element>) => void;
- /**
- * The callback for when Open in Discover is clicked.
- */
- handleOpenInDiscoverClick?: (e: React.MouseEvent<Element>) => void;
- referrer?: string;
- showTransactions?: TransactionFilterOptions;
- /**
- * A list of preferred table headers to use over the field names.
- */
- titles?: string[];
- trendView?: TrendView;
- };
- type TableRenderProps = Omit<React.ComponentProps<typeof Pagination>, 'size'> &
- React.ComponentProps<typeof TransactionsTable> & {
- header: React.ReactNode;
- paginationCursorSize: React.ComponentProps<typeof Pagination>['size'];
- target?: string;
- };
- function TableRender({
- pageLinks,
- onCursor,
- header,
- eventView,
- organization,
- isLoading,
- location,
- columnOrder,
- tableData,
- titles,
- generateLink,
- handleCellAction,
- referrer,
- useAggregateAlias,
- target,
- paginationCursorSize,
- }: TableRenderProps) {
- const query = decodeScalar(location.query.query, '');
- const display = decodeScalar(location.query.display, DisplayModes.DURATION);
- const performanceAtScaleContext = useContext(PerformanceAtScaleContext);
- const hasResults =
- tableData && tableData.data && tableData.meta && tableData.data.length > 0;
- useEffect(() => {
- if (!performanceAtScaleContext) {
- return;
- }
- // we are now only collecting analytics data from the transaction summary page
- // when the display mode is set to duration
- if (display !== DisplayModes.DURATION) {
- return;
- }
- if (isLoading || hasResults === null) {
- performanceAtScaleContext.setTransactionListTableData(undefined);
- return;
- }
- if (
- !hasResults === performanceAtScaleContext.transactionListTableData?.empty &&
- query === performanceAtScaleContext.transactionListTableData?.query
- ) {
- return;
- }
- performanceAtScaleContext.setTransactionListTableData({
- empty: !hasResults,
- query,
- });
- }, [display, isLoading, hasResults, performanceAtScaleContext, query]);
- const content = (
- <TransactionsTable
- eventView={eventView}
- organization={organization}
- location={location}
- isLoading={isLoading}
- tableData={tableData}
- columnOrder={columnOrder}
- titles={titles}
- generateLink={generateLink}
- handleCellAction={handleCellAction}
- useAggregateAlias={useAggregateAlias}
- referrer={referrer}
- />
- );
- return (
- <Fragment>
- <Header>
- {header}
- <StyledPagination
- pageLinks={pageLinks}
- onCursor={onCursor}
- size={paginationCursorSize}
- />
- </Header>
- {target ? (
- <GuideAnchor target={target} position="top-start">
- {content}
- </GuideAnchor>
- ) : (
- content
- )}
- </Fragment>
- );
- }
- class _TransactionsList extends Component<Props> {
- static defaultProps = {
- cursorName: 'transactionCursor',
- limit: DEFAULT_TRANSACTION_LIMIT,
- };
- handleCursor: CursorHandler = (cursor, pathname, query) => {
- const {cursorName} = this.props;
- browserHistory.push({
- pathname,
- query: {...query, [cursorName]: cursor},
- });
- };
- getEventView() {
- const {eventView, selected} = this.props;
- const sortedEventView = eventView.withSorts([selected.sort]);
- if (selected.query) {
- const query = new MutableSearch(sortedEventView.query);
- selected.query.forEach(item => query.setFilterValues(item[0], [item[1]]));
- sortedEventView.query = query.formatString();
- }
- return sortedEventView;
- }
- generateDiscoverEventView(): EventView {
- const {generateDiscoverEventView} = this.props;
- if (typeof generateDiscoverEventView === 'function') {
- return generateDiscoverEventView();
- }
- return this.getEventView();
- }
- generatePerformanceTransactionEventsView(): EventView {
- const {generatePerformanceTransactionEventsView} = this.props;
- return generatePerformanceTransactionEventsView?.() ?? this.getEventView();
- }
- renderHeader(): React.ReactNode {
- const {
- organization,
- selected,
- options,
- handleDropdownChange,
- handleOpenAllEventsClick,
- handleOpenInDiscoverClick,
- showTransactions,
- breakdown,
- } = this.props;
- return (
- <Fragment>
- <div>
- <CompactSelect
- triggerProps={{prefix: t('Filter'), size: 'xs'}}
- value={selected.value}
- options={options}
- onChange={opt => handleDropdownChange(opt.value)}
- />
- </div>
- {!this.isTrend() &&
- (handleOpenAllEventsClick ? (
- <GuideAnchor target="release_transactions_open_in_transaction_events">
- <Button
- onClick={handleOpenAllEventsClick}
- to={this.generatePerformanceTransactionEventsView().getPerformanceTransactionEventsViewUrlTarget(
- organization.slug,
- {
- showTransactions: mapShowTransactionToPercentile(showTransactions),
- breakdown,
- }
- )}
- size="xs"
- data-test-id="transaction-events-open"
- >
- {t('View All Events')}
- </Button>
- </GuideAnchor>
- ) : (
- <GuideAnchor target="release_transactions_open_in_discover">
- <DiscoverButton
- onClick={handleOpenInDiscoverClick}
- to={this.generateDiscoverEventView().getResultsViewUrlTarget(
- organization.slug
- )}
- size="xs"
- data-test-id="discover-open"
- >
- {t('Open in Discover')}
- </DiscoverButton>
- </GuideAnchor>
- ))}
- </Fragment>
- );
- }
- renderTransactionTable(): React.ReactNode {
- const {
- location,
- organization,
- handleCellAction,
- cursorName,
- limit,
- titles,
- generateLink,
- forceLoading,
- referrer,
- } = this.props;
- const eventView = this.getEventView();
- const columnOrder = eventView.getColumns();
- const cursor = decodeScalar(location.query?.[cursorName]);
- const tableCommonProps: Omit<
- TableRenderProps,
- 'isLoading' | 'pageLinks' | 'tableData'
- > = {
- handleCellAction,
- referrer,
- eventView,
- organization,
- location,
- columnOrder,
- titles,
- generateLink,
- useAggregateAlias: false,
- header: this.renderHeader(),
- target: 'transactions_table',
- paginationCursorSize: 'xs',
- onCursor: this.handleCursor,
- };
- if (forceLoading) {
- return (
- <TableRender {...tableCommonProps} isLoading pageLinks={null} tableData={null} />
- );
- }
- return (
- <DiscoverQuery
- location={location}
- eventView={eventView}
- orgSlug={organization.slug}
- limit={limit}
- cursor={cursor}
- referrer="api.discover.transactions-list"
- >
- {({isLoading, pageLinks, tableData}) => (
- <TableRender
- {...tableCommonProps}
- isLoading={isLoading}
- pageLinks={pageLinks}
- tableData={tableData}
- />
- )}
- </DiscoverQuery>
- );
- }
- renderTrendsTable(): React.ReactNode {
- const {trendView, location, selected, organization, cursorName, generateLink} =
- this.props;
- const sortedEventView: TrendView = trendView!.clone();
- sortedEventView.sorts = [selected.sort];
- sortedEventView.trendType = selected.trendType;
- if (selected.query) {
- const query = new MutableSearch(sortedEventView.query);
- selected.query.forEach(item => query.setFilterValues(item[0], [item[1]]));
- sortedEventView.query = query.formatString();
- }
- const cursor = decodeScalar(location.query?.[cursorName]);
- return (
- <TrendsEventsDiscoverQuery
- eventView={sortedEventView}
- orgSlug={organization.slug}
- location={location}
- cursor={cursor}
- limit={5}
- >
- {({isLoading, trendsData, pageLinks}) => (
- <TableRender
- organization={organization}
- eventView={sortedEventView}
- location={location}
- isLoading={isLoading}
- tableData={trendsData}
- pageLinks={pageLinks}
- onCursor={this.handleCursor}
- paginationCursorSize="sm"
- header={this.renderHeader()}
- titles={['transaction', 'percentage', 'difference']}
- columnOrder={decodeColumnOrder([
- {field: 'transaction'},
- {field: 'trend_percentage()'},
- {field: 'trend_difference()'},
- ])}
- generateLink={generateLink}
- useAggregateAlias
- />
- )}
- </TrendsEventsDiscoverQuery>
- );
- }
- isTrend(): boolean {
- const {selected} = this.props;
- return selected.trendType !== undefined;
- }
- render() {
- return (
- <Fragment>
- {this.isTrend() ? this.renderTrendsTable() : this.renderTransactionTable()}
- </Fragment>
- );
- }
- }
- const Header = styled('div')`
- display: grid;
- grid-template-columns: 1fr auto auto;
- margin-bottom: ${space(1)};
- align-items: center;
- `;
- const StyledPagination = styled(Pagination)`
- margin: 0 0 0 ${space(1)};
- `;
- function TransactionsList(
- props: Omit<Props, 'cursorName' | 'limit'> & {
- cursorName?: Props['cursorName'];
- limit?: Props['limit'];
- }
- ) {
- return <_TransactionsList {...props} />;
- }
- export default TransactionsList;
|