123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370 |
- import {Fragment} from 'react';
- import styled from '@emotion/styled';
- import {Location, LocationDescriptorObject} from 'history';
- import trimStart from 'lodash/trimStart';
- import {GridColumnOrder} from 'sentry/components/gridEditable';
- import SortLink from 'sentry/components/gridEditable/sortLink';
- import Link from 'sentry/components/links/link';
- import {Tooltip} from 'sentry/components/tooltip';
- import {t} from 'sentry/locale';
- import {Organization, PageFilters, Project} from 'sentry/types';
- import {defined} from 'sentry/utils';
- import {trackAnalytics} from 'sentry/utils/analytics';
- import {
- getIssueFieldRenderer,
- getSortField,
- } from 'sentry/utils/dashboards/issueFieldRenderers';
- import {TableDataRow, TableDataWithTitle} from 'sentry/utils/discover/discoverQuery';
- import EventView, {isFieldSortable} from 'sentry/utils/discover/eventView';
- import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
- import {
- fieldAlignment,
- getAggregateAlias,
- getEquationAliasIndex,
- isAggregateField,
- isEquationAlias,
- Sort,
- } from 'sentry/utils/discover/fields';
- import {
- eventDetailsRouteWithEventView,
- generateEventSlug,
- } from 'sentry/utils/discover/urls';
- import {DisplayType, Widget, WidgetType} from 'sentry/views/dashboards/types';
- import {eventViewFromWidget} from 'sentry/views/dashboards/utils';
- import {ISSUE_FIELDS} from 'sentry/views/dashboards/widgetBuilder/issueWidget/fields';
- import {TransactionLink} from 'sentry/views/discover/table/tableView';
- import TopResultsIndicator from 'sentry/views/discover/table/topResultsIndicator';
- import {TableColumn} from 'sentry/views/discover/table/types';
- import {getTargetForTransactionSummaryLink} from 'sentry/views/discover/utils';
- import {WidgetViewerQueryField} from './utils';
- // Dashboards only supports top 5 for now
- const DEFAULT_NUM_TOP_EVENTS = 5;
- type Props = {
- location: Location;
- organization: Organization;
- selection: PageFilters;
- widget: Widget;
- eventView?: EventView;
- isFirstPage?: boolean;
- isMetricsData?: boolean;
- onHeaderClick?: () => void;
- projects?: Project[];
- tableData?: TableDataWithTitle;
- };
- export const renderIssueGridHeaderCell = ({
- location,
- widget,
- tableData,
- organization,
- onHeaderClick,
- }: Props) =>
- function (
- column: TableColumn<keyof TableDataRow>,
- _columnIndex: number
- ): React.ReactNode {
- const tableMeta = tableData?.meta;
- const align = fieldAlignment(column.name, column.type, tableMeta);
- const sortField = getSortField(String(column.key));
- return (
- <SortLink
- align={align}
- title={<StyledTooltip title={column.name}>{column.name}</StyledTooltip>}
- direction={widget.queries[0].orderby === sortField ? 'desc' : undefined}
- canSort={!!sortField}
- generateSortLink={() => ({
- ...location,
- query: {
- ...location.query,
- [WidgetViewerQueryField.SORT]: sortField,
- [WidgetViewerQueryField.PAGE]: undefined,
- [WidgetViewerQueryField.CURSOR]: undefined,
- },
- })}
- onClick={() => {
- onHeaderClick?.();
- trackAnalytics('dashboards_views.widget_viewer.sort', {
- organization,
- widget_type: WidgetType.ISSUE,
- display_type: widget.displayType,
- column: column.name,
- order: 'desc',
- });
- }}
- />
- );
- };
- export const renderDiscoverGridHeaderCell = ({
- location,
- selection,
- widget,
- tableData,
- organization,
- onHeaderClick,
- isMetricsData,
- }: Props) =>
- function (
- column: TableColumn<keyof TableDataRow>,
- _columnIndex: number
- ): React.ReactNode {
- const {orderby} = widget.queries[0];
- // Need to convert orderby to aggregate alias because eventView still uses aggregate alias format
- const aggregateAliasOrderBy = `${
- orderby.startsWith('-') ? '-' : ''
- }${getAggregateAlias(trimStart(orderby, '-'))}`;
- const eventView = eventViewFromWidget(
- widget.title,
- {...widget.queries[0], orderby: aggregateAliasOrderBy},
- selection,
- widget.displayType
- );
- const tableMeta = tableData?.meta;
- const align = fieldAlignment(column.name, column.type, tableMeta);
- const field = {field: String(column.key), width: column.width};
- function generateSortLink(): LocationDescriptorObject | undefined {
- if (!tableMeta) {
- return undefined;
- }
- const nextEventView = eventView.sortOnField(field, tableMeta, undefined, true);
- const queryStringObject = nextEventView.generateQueryStringObject();
- return {
- ...location,
- query: {
- ...location.query,
- [WidgetViewerQueryField.SORT]: queryStringObject.sort,
- [WidgetViewerQueryField.PAGE]: undefined,
- [WidgetViewerQueryField.CURSOR]: undefined,
- },
- };
- }
- const currentSort = eventView.sortForField(field, tableMeta);
- const canSort =
- !(isMetricsData && field.field === 'title') && isFieldSortable(field, tableMeta);
- const titleText = isEquationAlias(column.name)
- ? eventView.getEquations()[getEquationAliasIndex(column.name)]
- : column.name;
- return (
- <SortLink
- align={align}
- title={<StyledTooltip title={titleText}>{titleText}</StyledTooltip>}
- direction={currentSort ? currentSort.kind : undefined}
- canSort={canSort}
- generateSortLink={generateSortLink}
- onClick={() => {
- onHeaderClick?.();
- trackAnalytics('dashboards_views.widget_viewer.sort', {
- organization,
- widget_type: WidgetType.DISCOVER,
- display_type: widget.displayType,
- column: column.name,
- order: currentSort?.kind === 'desc' ? 'asc' : 'desc',
- });
- }}
- />
- );
- };
- export const renderGridBodyCell = ({
- location,
- organization,
- widget,
- tableData,
- isFirstPage,
- projects,
- eventView,
- }: Props) =>
- function (
- column: GridColumnOrder,
- dataRow: Record<string, any>,
- rowIndex: number,
- columnIndex: number
- ): React.ReactNode {
- const columnKey = String(column.key);
- const isTopEvents = widget.displayType === DisplayType.TOP_N;
- let cell: React.ReactNode;
- switch (widget.widgetType) {
- case WidgetType.ISSUE:
- cell = (
- getIssueFieldRenderer(columnKey) ?? getFieldRenderer(columnKey, ISSUE_FIELDS)
- )(dataRow, {organization, location});
- break;
- case WidgetType.DISCOVER:
- default:
- if (!tableData || !tableData.meta) {
- return dataRow[column.key];
- }
- const unit = tableData.meta.units?.[column.key];
- cell = getFieldRenderer(
- columnKey,
- tableData.meta,
- false
- )(dataRow, {
- organization,
- location,
- unit,
- });
- const fieldName = getAggregateAlias(columnKey);
- const value = dataRow[fieldName];
- if (tableData.meta[fieldName] === 'integer' && defined(value) && value > 999) {
- return (
- <Tooltip
- title={value.toLocaleString()}
- containerDisplayMode="block"
- position="right"
- >
- {cell}
- </Tooltip>
- );
- }
- break;
- }
- if (columnKey === 'transaction' && dataRow.transaction) {
- cell = (
- <TransactionLink
- data-test-id="widget-viewer-transaction-link"
- to={getTargetForTransactionSummaryLink(
- dataRow,
- organization,
- projects,
- eventView
- )}
- >
- {cell}
- </TransactionLink>
- );
- }
- const topResultsCount = tableData
- ? Math.min(tableData?.data.length, DEFAULT_NUM_TOP_EVENTS)
- : DEFAULT_NUM_TOP_EVENTS;
- return (
- <Fragment>
- {isTopEvents &&
- isFirstPage &&
- rowIndex < DEFAULT_NUM_TOP_EVENTS &&
- columnIndex === 0 ? (
- <TopResultsIndicator count={topResultsCount} index={rowIndex} />
- ) : null}
- {cell}
- </Fragment>
- );
- };
- export const renderPrependColumns =
- ({location, organization, tableData, eventView}: Props & {eventView: EventView}) =>
- (isHeader: boolean, dataRow?: any, rowIndex?: number): React.ReactNode[] => {
- if (isHeader) {
- return [
- <PrependHeader key="header-event-id">
- <SortLink
- align="left"
- title={t('event id')}
- direction={undefined}
- canSort={false}
- generateSortLink={() => undefined}
- />
- </PrependHeader>,
- ];
- }
- let value = dataRow.id;
- if (tableData?.meta) {
- const fieldRenderer = getFieldRenderer('id', tableData?.meta);
- value = fieldRenderer(dataRow, {organization, location});
- }
- const eventSlug = generateEventSlug(dataRow);
- const target = eventDetailsRouteWithEventView({
- orgSlug: organization.slug,
- eventSlug,
- eventView,
- });
- return [
- <Tooltip key={`eventlink${rowIndex}`} title={t('View Event')}>
- <Link data-test-id="view-event" to={target}>
- {value}
- </Link>
- </Tooltip>,
- ];
- };
- export const renderReleaseGridHeaderCell = ({
- location,
- widget,
- tableData,
- organization,
- onHeaderClick,
- }: Props) =>
- function (
- column: TableColumn<keyof TableDataRow>,
- _columnIndex: number
- ): React.ReactNode {
- const tableMeta = tableData?.meta;
- const align = fieldAlignment(column.name, column.type, tableMeta);
- const widgetOrderBy = widget.queries[0].orderby;
- const sort: Sort = {
- kind: widgetOrderBy.startsWith('-') ? 'desc' : 'asc',
- field: widgetOrderBy.startsWith('-') ? widgetOrderBy.slice(1) : widgetOrderBy,
- };
- const canSort = isAggregateField(column.name);
- const titleText = column.name;
- function generateSortLink(): LocationDescriptorObject {
- const columnSort =
- column.name === sort.field
- ? {...sort, kind: sort.kind === 'desc' ? 'asc' : 'desc'}
- : {kind: 'desc', field: column.name};
- return {
- ...location,
- query: {
- ...location.query,
- [WidgetViewerQueryField.SORT]:
- columnSort.kind === 'desc' ? `-${columnSort.field}` : columnSort.field,
- [WidgetViewerQueryField.PAGE]: undefined,
- [WidgetViewerQueryField.CURSOR]: undefined,
- },
- };
- }
- return (
- <SortLink
- align={align}
- title={<StyledTooltip title={titleText}>{titleText}</StyledTooltip>}
- direction={sort.field === column.name ? sort.kind : undefined}
- canSort={canSort}
- generateSortLink={generateSortLink}
- onClick={() => {
- onHeaderClick?.();
- trackAnalytics('dashboards_views.widget_viewer.sort', {
- organization,
- widget_type: WidgetType.RELEASE,
- display_type: widget.displayType,
- column: column.name,
- order: sort?.kind === 'desc' ? 'asc' : 'desc',
- });
- }}
- />
- );
- };
- const StyledTooltip = styled(Tooltip)`
- display: initial;
- `;
- const PrependHeader = styled('span')`
- color: ${p => p.theme.subText};
- `;
|