|
@@ -0,0 +1,266 @@
|
|
|
+import {Fragment, useMemo, useState} from 'react';
|
|
|
+import {useTheme} from '@emotion/react';
|
|
|
+import pick from 'lodash/pick';
|
|
|
+
|
|
|
+import {BarChart} from 'sentry/components/charts/barChart';
|
|
|
+import _EventsRequest from 'sentry/components/charts/eventsRequest';
|
|
|
+import {getInterval} from 'sentry/components/charts/utils';
|
|
|
+import Count from 'sentry/components/count';
|
|
|
+import Truncate from 'sentry/components/truncate';
|
|
|
+import {t} from 'sentry/locale';
|
|
|
+import {tooltipFormatter} from 'sentry/utils/discover/charts';
|
|
|
+import DiscoverQuery from 'sentry/utils/discover/discoverQuery';
|
|
|
+import {
|
|
|
+ canUseMetricsData,
|
|
|
+ useMEPSettingContext,
|
|
|
+} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
|
|
|
+import {usePageError} from 'sentry/utils/performance/contexts/pageError';
|
|
|
+import {MutableSearch} from 'sentry/utils/tokenizeSearch';
|
|
|
+import {useLocation} from 'sentry/utils/useLocation';
|
|
|
+import withApi from 'sentry/utils/withApi';
|
|
|
+import {transactionSummaryRouteWithQuery} from 'sentry/views/performance/transactionSummary/utils';
|
|
|
+import {
|
|
|
+ createUnnamedTransactionsDiscoverTarget,
|
|
|
+ UNPARAMETERIZED_TRANSACTION,
|
|
|
+} from 'sentry/views/performance/utils';
|
|
|
+
|
|
|
+import Accordion from '../components/accordion';
|
|
|
+import {GenericPerformanceWidget} from '../components/performanceWidget';
|
|
|
+import {
|
|
|
+ GrowLink,
|
|
|
+ RightAlignedCell,
|
|
|
+ Subtitle,
|
|
|
+ WidgetEmptyStateWarning,
|
|
|
+} from '../components/selectableList';
|
|
|
+import {transformDiscoverToList} from '../transforms/transformDiscoverToList';
|
|
|
+import {transformEventsRequestToStackedBars} from '../transforms/transformEventsToStackedBars';
|
|
|
+import {PerformanceWidgetProps, QueryDefinition, WidgetDataResult} from '../types';
|
|
|
+import {eventsRequestQueryProps, getMEPParamsIfApplicable} from '../utils';
|
|
|
+
|
|
|
+type DataType = {
|
|
|
+ chart: WidgetDataResult & ReturnType<typeof transformEventsRequestToStackedBars>;
|
|
|
+ list: WidgetDataResult & ReturnType<typeof transformDiscoverToList>;
|
|
|
+};
|
|
|
+
|
|
|
+export function StackedBarsChartListWidget(props: PerformanceWidgetProps) {
|
|
|
+ const location = useLocation();
|
|
|
+ const mepSetting = useMEPSettingContext();
|
|
|
+ const [selectedListIndex, setSelectListIndex] = useState<number>(0);
|
|
|
+ const {ContainerActions, organization, InteractiveTitle, fields} = props;
|
|
|
+ const pageError = usePageError();
|
|
|
+ const theme = useTheme();
|
|
|
+
|
|
|
+ const colors = [...theme.charts.getColorPalette(5)].reverse();
|
|
|
+
|
|
|
+ const listQuery = useMemo<QueryDefinition<DataType, WidgetDataResult>>(
|
|
|
+ () => ({
|
|
|
+ fields,
|
|
|
+ component: provided => {
|
|
|
+ const eventView = provided.eventView.clone();
|
|
|
+
|
|
|
+ eventView.fields = [
|
|
|
+ {field: 'transaction'},
|
|
|
+ {field: 'team_key_transaction'},
|
|
|
+ {field: 'count()'},
|
|
|
+ {field: 'project.id'},
|
|
|
+ ...fields.map(f => ({field: f})),
|
|
|
+ ];
|
|
|
+
|
|
|
+ eventView.sorts = [
|
|
|
+ {kind: 'desc', field: 'team_key_transaction'},
|
|
|
+ {kind: 'desc', field: 'count()'},
|
|
|
+ ];
|
|
|
+
|
|
|
+ if (canUseMetricsData(organization)) {
|
|
|
+ eventView.additionalConditions.setFilterValues('!transaction', [
|
|
|
+ UNPARAMETERIZED_TRANSACTION,
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ const mutableSearch = new MutableSearch(eventView.query);
|
|
|
+ mutableSearch.removeFilter('transaction.duration');
|
|
|
+
|
|
|
+ eventView.query = mutableSearch.formatString();
|
|
|
+
|
|
|
+ // Don't retrieve list items with 0 in the field.
|
|
|
+ eventView.additionalConditions.setFilterValues('count()', ['>0']);
|
|
|
+ eventView.additionalConditions.setFilterValues('!transaction.op', ['']);
|
|
|
+ return (
|
|
|
+ <DiscoverQuery
|
|
|
+ {...provided}
|
|
|
+ eventView={eventView}
|
|
|
+ location={location}
|
|
|
+ limit={3}
|
|
|
+ cursor="0:0:1"
|
|
|
+ noPagination
|
|
|
+ queryExtras={getMEPParamsIfApplicable(mepSetting, props.chartSetting)}
|
|
|
+ />
|
|
|
+ );
|
|
|
+ },
|
|
|
+ transform: transformDiscoverToList,
|
|
|
+ }),
|
|
|
+ // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
+ [props.chartSetting, mepSetting.memoizationKey]
|
|
|
+ );
|
|
|
+
|
|
|
+ const chartQuery = useMemo<QueryDefinition<DataType, WidgetDataResult>>(
|
|
|
+ () => {
|
|
|
+ return {
|
|
|
+ enabled: widgetData => {
|
|
|
+ return !!widgetData?.list?.data?.length;
|
|
|
+ },
|
|
|
+ fields,
|
|
|
+ component: provided => {
|
|
|
+ const eventView = props.eventView.clone();
|
|
|
+ if (!provided.widgetData.list.data[selectedListIndex]?.transaction) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ eventView.additionalConditions.setFilterValues('transaction', [
|
|
|
+ provided.widgetData.list.data[selectedListIndex].transaction as string,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ if (canUseMetricsData(organization)) {
|
|
|
+ eventView.additionalConditions.setFilterValues('!transaction', [
|
|
|
+ UNPARAMETERIZED_TRANSACTION,
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ const listResult = provided.widgetData.list.data[selectedListIndex];
|
|
|
+ const nonEmptySpanOpFields = Object.entries(listResult)
|
|
|
+ .filter(result => fields.includes(result[0]) && result[1] !== 0)
|
|
|
+ .map(result => result[0]);
|
|
|
+ const prunedProvided = {...provided, yAxis: nonEmptySpanOpFields};
|
|
|
+
|
|
|
+ return (
|
|
|
+ <EventsRequest
|
|
|
+ {...pick(prunedProvided, eventsRequestQueryProps)}
|
|
|
+ limit={5}
|
|
|
+ includePrevious={false}
|
|
|
+ includeTransformedData
|
|
|
+ partial
|
|
|
+ currentSeriesNames={nonEmptySpanOpFields}
|
|
|
+ query={eventView.getQueryWithAdditionalConditions()}
|
|
|
+ interval={getInterval(
|
|
|
+ {
|
|
|
+ start: prunedProvided.start,
|
|
|
+ end: prunedProvided.end,
|
|
|
+ period: prunedProvided.period,
|
|
|
+ },
|
|
|
+ 'low'
|
|
|
+ )}
|
|
|
+ hideError
|
|
|
+ onError={pageError.setPageError}
|
|
|
+ queryExtras={getMEPParamsIfApplicable(mepSetting, props.chartSetting)}
|
|
|
+ />
|
|
|
+ );
|
|
|
+ },
|
|
|
+ transform: transformEventsRequestToStackedBars,
|
|
|
+ };
|
|
|
+ },
|
|
|
+ // eslint-disable-next-line react-hooks/exhaustive-deps
|
|
|
+ [props.chartSetting, selectedListIndex, mepSetting.memoizationKey]
|
|
|
+ );
|
|
|
+
|
|
|
+ const Queries = {
|
|
|
+ list: listQuery,
|
|
|
+ chart: chartQuery,
|
|
|
+ };
|
|
|
+
|
|
|
+ const getHeaders = provided =>
|
|
|
+ provided.widgetData.list.data.map(listItem => () => {
|
|
|
+ const transaction = (listItem.transaction as string | undefined) ?? '';
|
|
|
+
|
|
|
+ const isUnparameterizedRow = transaction === UNPARAMETERIZED_TRANSACTION;
|
|
|
+ const transactionTarget = isUnparameterizedRow
|
|
|
+ ? createUnnamedTransactionsDiscoverTarget({
|
|
|
+ organization,
|
|
|
+ location,
|
|
|
+ })
|
|
|
+ : transactionSummaryRouteWithQuery({
|
|
|
+ orgSlug: props.organization.slug,
|
|
|
+ projectID: listItem['project.id'] as string,
|
|
|
+ transaction,
|
|
|
+ query: props.eventView.generateQueryStringObject(),
|
|
|
+ subPath: 'spans',
|
|
|
+ });
|
|
|
+
|
|
|
+ const displayedField = 'count()';
|
|
|
+ const rightValue = listItem[displayedField];
|
|
|
+
|
|
|
+ return (
|
|
|
+ <Fragment>
|
|
|
+ <GrowLink to={transactionTarget}>
|
|
|
+ <Truncate value={transaction} maxLength={40} />
|
|
|
+ </GrowLink>
|
|
|
+ <RightAlignedCell>
|
|
|
+ <Count value={rightValue} />
|
|
|
+ </RightAlignedCell>
|
|
|
+ </Fragment>
|
|
|
+ );
|
|
|
+ });
|
|
|
+
|
|
|
+ return (
|
|
|
+ <GenericPerformanceWidget<DataType>
|
|
|
+ {...props}
|
|
|
+ location={location}
|
|
|
+ Subtitle={() => <Subtitle>{t('Top transactions in count')}</Subtitle>}
|
|
|
+ HeaderActions={provided =>
|
|
|
+ ContainerActions && (
|
|
|
+ <ContainerActions isLoading={provided.widgetData.list?.isLoading} />
|
|
|
+ )
|
|
|
+ }
|
|
|
+ InteractiveTitle={
|
|
|
+ InteractiveTitle
|
|
|
+ ? provided => <InteractiveTitle {...provided.widgetData.chart} />
|
|
|
+ : null
|
|
|
+ }
|
|
|
+ EmptyComponent={WidgetEmptyStateWarning}
|
|
|
+ Queries={Queries}
|
|
|
+ Visualizations={[
|
|
|
+ {
|
|
|
+ component: provided => {
|
|
|
+ return (
|
|
|
+ <Accordion
|
|
|
+ expandedIndex={selectedListIndex}
|
|
|
+ setExpandedIndex={setSelectListIndex}
|
|
|
+ content={
|
|
|
+ <BarChart
|
|
|
+ {...provided.widgetData.chart}
|
|
|
+ {...provided}
|
|
|
+ colors={colors}
|
|
|
+ series={provided.widgetData.chart.data}
|
|
|
+ stacked
|
|
|
+ animation
|
|
|
+ isGroupedByDate
|
|
|
+ showTimeInTooltip
|
|
|
+ xAxis={{
|
|
|
+ show: false,
|
|
|
+ axisLabel: {show: true, margin: 8},
|
|
|
+ axisLine: {show: false},
|
|
|
+ }}
|
|
|
+ tooltip={{
|
|
|
+ valueFormatter: value => tooltipFormatter(value, 'duration'),
|
|
|
+ }}
|
|
|
+ start={
|
|
|
+ provided.widgetData.chart.start
|
|
|
+ ? new Date(provided.widgetData.chart.start)
|
|
|
+ : undefined
|
|
|
+ }
|
|
|
+ end={
|
|
|
+ provided.widgetData.chart.end
|
|
|
+ ? new Date(provided.widgetData.chart.end)
|
|
|
+ : undefined
|
|
|
+ }
|
|
|
+ />
|
|
|
+ }
|
|
|
+ headers={getHeaders(provided)}
|
|
|
+ />
|
|
|
+ );
|
|
|
+ },
|
|
|
+ height: 124 + props.chartHeight,
|
|
|
+ noPadding: true,
|
|
|
+ },
|
|
|
+ ]}
|
|
|
+ />
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+const EventsRequest = withApi(_EventsRequest);
|