Skip to content

ref(explore): add some missing default renderers to getFieldRenderer #95231

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions static/app/utils/dashboards/issueFieldRenderers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import {Container, FieldShortId, OverflowLink} from 'sentry/utils/discover/style
import {SavedQueryDatasets} from 'sentry/utils/discover/types';
import {hasDatasetSelector} from 'sentry/views/dashboards/utils';
import {FieldKey} from 'sentry/views/dashboards/widgetBuilder/issueWidget/fields';
import {QuickContextHoverWrapper} from 'sentry/views/discover/table/quickContext/quickContextWrapper';
import {ContextType} from 'sentry/views/discover/table/quickContext/utils';

/**
* Types, functions and definitions for rendering fields in discover results.
Expand Down Expand Up @@ -79,9 +81,15 @@ const SPECIAL_FIELDS: SpecialFields = {

return (
<Container>
<OverflowLink to={target} aria-label={issueID}>
<FieldShortId shortId={`${data.issue}`} />
</OverflowLink>
<QuickContextHoverWrapper
dataRow={data}
contextType={ContextType.ISSUE}
organization={organization}
>
<OverflowLink to={target} aria-label={issueID}>
<FieldShortId shortId={`${data.issue}`} />
</OverflowLink>
</QuickContextHoverWrapper>
</Container>
);
},
Expand Down
220 changes: 180 additions & 40 deletions static/app/utils/discover/fieldRenderers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@ import {deviceNameMapper} from 'sentry/components/deviceName';
import type {MenuItemProps} from 'sentry/components/dropdownMenu';
import {DropdownMenu} from 'sentry/components/dropdownMenu';
import Duration from 'sentry/components/duration';
import {ContextIcon} from 'sentry/components/events/contexts/contextIcon';
import FileSize from 'sentry/components/fileSize';
import BadgeDisplayName from 'sentry/components/idBadge/badgeDisplayName';
import ProjectBadge from 'sentry/components/idBadge/projectBadge';
import UserBadge from 'sentry/components/idBadge/userBadge';
import ExternalLink from 'sentry/components/links/externalLink';
import {RowRectangle} from 'sentry/components/performance/waterfall/rowBar';
import {pickBarColor} from 'sentry/components/performance/waterfall/utils';
import TimeSince from 'sentry/components/timeSince';
import UserMisery from 'sentry/components/userMisery';
import Version from 'sentry/components/version';
import {IconDownload} from 'sentry/icons';
Expand All @@ -45,6 +47,7 @@ import {
SPAN_OP_BREAKDOWN_FIELDS,
SPAN_OP_RELATIVE_BREAKDOWN_FIELD,
} from 'sentry/utils/discover/fields';
import ViewReplayLink from 'sentry/utils/discover/viewReplayLink';
import {getShortEventId} from 'sentry/utils/events';
import {formatRate} from 'sentry/utils/formatters';
import getDynamicText from 'sentry/utils/getDynamicText';
Expand All @@ -60,15 +63,18 @@ import {ContextType} from 'sentry/views/discover/table/quickContext/utils';
import {PerformanceBadge} from 'sentry/views/insights/browser/webVitals/components/performanceBadge';
import {PercentChangeCell} from 'sentry/views/insights/common/components/tableCells/percentChangeCell';
import {ResponseStatusCodeCell} from 'sentry/views/insights/common/components/tableCells/responseStatusCodeCell';
import {SpanDescriptionCell} from 'sentry/views/insights/common/components/tableCells/spanDescriptionCell';
import {StarredSegmentCell} from 'sentry/views/insights/common/components/tableCells/starredSegmentCell';
import {TimeSpentCell} from 'sentry/views/insights/common/components/tableCells/timeSpentCell';
import {SpanFields, SpanMetricsField} from 'sentry/views/insights/types';
import {ModuleName, SpanFields, SpanMetricsField} from 'sentry/views/insights/types';
import {
filterToLocationQuery,
SpanOperationBreakdownFilter,
stringToFilter,
} from 'sentry/views/performance/transactionSummary/filter';
import {makeProjectsPathname} from 'sentry/views/projects/pathname';
import {ADOPTION_STAGE_LABELS} from 'sentry/views/releases/utils';
import {makeReplaysPathname} from 'sentry/views/replays/pathnames';

import ArrayValue from './arrayValue';
import {
Expand All @@ -77,6 +83,7 @@ import {
FieldDateTime,
FieldShortId,
FlexContainer,
IconContainer,
NumberContainer,
OverflowFieldShortId,
OverflowLink,
Expand Down Expand Up @@ -134,7 +141,9 @@ const EmptyValueContainer = styled('span')`
color: ${p => p.theme.subText};
`;
const emptyValue = <EmptyValueContainer>{t('(no value)')}</EmptyValueContainer>;
const emptyStringValue = <EmptyValueContainer>{t('(empty string)')}</EmptyValueContainer>;
export const emptyStringValue = (
<EmptyValueContainer>{t('(empty string)')}</EmptyValueContainer>
);
const missingUserMisery = tct(
'We were unable to calculate User Misery. A likely cause of this is that the user was not set. [link:Read the docs]',
{
Expand All @@ -143,6 +152,9 @@ const missingUserMisery = tct(
),
}
);
const userAgentLocking = t(
'This OS locks newer versions to this version in the user-agent HTTP header. The exact OS version is unknown.'
);

export function nullableValue(value: string | null): string | React.ReactElement {
switch (value) {
Expand Down Expand Up @@ -367,35 +379,6 @@ type SpecialField = {
sortField: string | null;
};

type SpecialFields = {
adoption_stage: SpecialField;
'apdex()': SpecialField;
attachments: SpecialField;
'count_unique(user)': SpecialField;
device: SpecialField;
'error.handled': SpecialField;
id: SpecialField;
[SpanFields.IS_STARRED_TRANSACTION]: SpecialField;
issue: SpecialField;
'issue.id': SpecialField;
minidump: SpecialField;
'performance_score(measurements.score.total)': SpecialField;
'profile.id': SpecialField;
project: SpecialField;
release: SpecialField;
replayId: SpecialField;
'span.description': SpecialField;
'span.status_code': SpecialField;
span_id: SpecialField;
team_key_transaction: SpecialField;
'timestamp.to_day': SpecialField;
'timestamp.to_hour': SpecialField;
trace: SpecialField;
'trend_percentage()': SpecialField;
user: SpecialField;
'user.display': SpecialField;
};

const DownloadCount = styled('span')`
padding-left: ${space(0.75)};
`;
Expand All @@ -409,7 +392,7 @@ const RightAlignedContainer = styled('span')`
* "Special fields" either do not map 1:1 to an single column in the event database,
* or they require custom UI formatting that can't be handled by the datatype formatters.
*/
const SPECIAL_FIELDS: SpecialFields = {
const SPECIAL_FIELDS: Record<string, SpecialField> = {
// This is a custom renderer for a field outside discover
// TODO - refactor code and remove from this file or add ability to query for attachments in Discover
'apdex()': {
Expand Down Expand Up @@ -496,9 +479,8 @@ const SPECIAL_FIELDS: SpecialFields = {
renderFunc: data => {
const id: string | unknown = data?.id;
if (typeof id !== 'string') {
return null;
return <Container>{emptyStringValue}</Container>;
}

return <Container>{getShortEventId(id)}</Container>;
},
},
Expand All @@ -517,7 +499,33 @@ const SPECIAL_FIELDS: SpecialFields = {
sortField: 'span.description',
renderFunc: data => {
const value = data['span.description'];

const op: string = data['span.op'];
const projectId =
typeof data.project_id === 'number'
? data.project_id
: parseInt(data.project_id, 10) || -1;
const spanGroup: string | undefined = data['span.group'];

if (op === ModuleName.DB) {
return (
<SpanDescriptionCell
description={value}
moduleName={op}
projectId={projectId}
group={spanGroup}
/>
);
}
if (op === ModuleName.RESOURCE) {
return (
<SpanDescriptionCell
description={value}
moduleName={op}
projectId={projectId}
group={spanGroup}
/>
);
}
return (
<Tooltip
title={value}
Expand Down Expand Up @@ -565,13 +573,24 @@ const SPECIAL_FIELDS: SpecialFields = {
},
replayId: {
sortField: 'replayId',
renderFunc: data => {
renderFunc: (data, {organization}) => {
const replayId = data?.replayId;
if (typeof replayId !== 'string' || !replayId) {
return emptyValue;
}

return <Container>{getShortEventId(replayId)}</Container>;
const target = makeReplaysPathname({
path: `/${replayId}/`,
organization,
});

return (
<Container>
<ViewReplayLink replayId={replayId} to={target}>
{getShortEventId(replayId)}
</ViewReplayLink>
</Container>
);
},
},
'profile.id': {
Expand Down Expand Up @@ -649,6 +668,31 @@ const SPECIAL_FIELDS: SpecialFields = {
);
},
},
project_id: {
sortField: 'project_id',
renderFunc: (data, {organization}) => {
const projectId = data.project_id;
// TODO: add projects to baggage to avoid using deprecated component
return (
<NumberContainer>
<Projects orgId={organization.slug} slugs={[]} projectIds={[projectId]}>
{({projects}) => {
const project = projects.find(p => p.id === projectId?.toString());
if (!project) {
return emptyValue;
}
const target = makeProjectsPathname({
path: `/${project?.slug}/?project=${projectId}/`,
organization,
});

return <Link to={target}>{projectId}</Link>;
}}
</Projects>
</NumberContainer>
);
},
},
user: {
sortField: 'user',
renderFunc: data => {
Expand Down Expand Up @@ -795,6 +839,21 @@ const SPECIAL_FIELDS: SpecialFields = {
</NumberContainer>
),
},
timestamp: {
sortField: 'timestamp',
renderFunc: data => {
const timestamp = data.timestamp;
if (!timestamp) {
return <Container>{emptyStringValue}</Container>;
}
const date = new Date(data.timestamp);
return (
<Container>
<TimeSince unitStyle="extraShort" date={date} tooltipShowSeconds />
</Container>
);
},
},
'timestamp.to_hour': {
sortField: 'timestamp.to_hour',
renderFunc: data => (
Expand Down Expand Up @@ -843,6 +902,87 @@ const SPECIAL_FIELDS: SpecialFields = {
);
},
},
'browser.name': {
sortField: 'browser.name',
renderFunc: data => {
const browserName: string = data['browser.name'];
if (!browserName) {
return <Container>{emptyStringValue}</Container>;
}

const formattedName = browserName.split(' ').join('-').toLocaleLowerCase();
return (
<IconContainer>
<ContextIcon name={formattedName} size="md" />
{browserName}
</IconContainer>
);
},
},
browser: {
sortField: 'browser',
renderFunc: data => {
const browser: string = data.browser;
if (!browser) {
return <Container>{emptyStringValue}</Container>;
}

const broswerArray = browser.split(' ');
broswerArray.pop();
const formattedName = broswerArray.join('-').toLocaleLowerCase();

return (
<IconContainer>
<ContextIcon name={formattedName} size="md" />
{browser}
</IconContainer>
);
},
},
'os.name': {
sortField: 'os.name',
renderFunc: data => {
const osName: string = data['os.name'];
if (!osName) {
return <Container>{emptyStringValue}</Container>;
}

const formattedName = osName.split(' ').join('-').toLocaleLowerCase();
return (
<IconContainer>
<ContextIcon name={formattedName} size="md" />
{osName}
</IconContainer>
);
},
},
os: {
sortField: 'os',
renderFunc: data => {
const os: string = data.os;
if (!os) {
return <Container>{emptyStringValue}</Container>;
}

const osArray = os.split(' ');
const hasUserAgentLocking = osArray[osArray.length - 1]?.includes('>=');
osArray.pop();
const formattedName = osArray.join('-').toLocaleLowerCase();

return (
<IconContainer>
<ContextIcon name={formattedName} size="md" />
{hasUserAgentLocking ? (
<Tooltip title={userAgentLocking} showUnderline>
{os}
</Tooltip>
) : (
os
)}
</IconContainer>
);
},
},
};

type SpecialFunctionFieldRenderer = (
Expand Down Expand Up @@ -951,8 +1091,8 @@ export function getSortField(
field: string,
tableMeta: MetaType | undefined
): string | null {
if (SPECIAL_FIELDS.hasOwnProperty(field)) {
return SPECIAL_FIELDS[field as keyof typeof SPECIAL_FIELDS].sortField;
if (Object.hasOwn(SPECIAL_FIELDS, field)) {
return SPECIAL_FIELDS[field]!.sortField;
}

if (!tableMeta) {
Expand All @@ -971,7 +1111,7 @@ export function getSortField(
}

const fieldType = tableMeta[field];
if (FIELD_FORMATTERS.hasOwnProperty(fieldType)) {
if (Object.hasOwn(FIELD_FORMATTERS, fieldType)) {
return FIELD_FORMATTERS[fieldType as keyof typeof FIELD_FORMATTERS].isSortable
? field
: null;
Expand Down
7 changes: 7 additions & 0 deletions static/app/utils/discover/styles.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,10 @@ export const UserIcon = styled(IconUser)`
margin-left: ${space(1)};
color: ${p => p.theme.gray400};
`;

export const IconContainer = styled('div')`
display: flex;
gap: ${space(1)};
overflow: hidden;
text-overflow: ellipsis;
`;
Loading
Loading