123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242 |
- import {Fragment} from 'react';
- import {withRouter, WithRouterProps} from 'react-router';
- import {css} from '@emotion/react';
- import styled from '@emotion/styled';
- import capitalize from 'lodash/capitalize';
- import ErrorBoundary from 'app/components/errorBoundary';
- import EventOrGroupTitle from 'app/components/eventOrGroupTitle';
- import GlobalSelectionLink from 'app/components/globalSelectionLink';
- import Tooltip from 'app/components/tooltip';
- import {IconMute, IconStar} from 'app/icons';
- import {tct} from 'app/locale';
- import {Group, GroupTombstone, Level, Organization} from 'app/types';
- import {Event} from 'app/types/event';
- import {getLocation, getMessage} from 'app/utils/events';
- import withOrganization from 'app/utils/withOrganization';
- import {TagAndMessageWrapper} from 'app/views/organizationGroupDetails/unhandledTag';
- import EventTitleError from './eventTitleError';
- type Size = 'small' | 'normal';
- type Props = WithRouterProps<{orgId: string}> & {
- organization: Organization;
- data: Event | Group | GroupTombstone;
- hideIcons?: boolean;
- hideLevel?: boolean;
- query?: string;
- className?: string;
- /** Group link clicked */
- onClick?: () => void;
- index?: number;
- includeLink?: boolean;
- size?: Size;
- };
- /**
- * Displays an event or group/issue title (i.e. in Stream)
- */
- function EventOrGroupHeader({
- data,
- index,
- organization,
- params,
- query,
- onClick,
- className,
- hideIcons,
- hideLevel,
- includeLink = true,
- size = 'normal',
- ...props
- }: Props) {
- const hasGroupingTreeUI = !!organization.features?.includes('grouping-tree-ui');
- function getTitleChildren() {
- const {level, status, isBookmarked, hasSeen} = data as Group;
- return (
- <Fragment>
- {!hideLevel && level && (
- <GroupLevel level={level}>
- <Tooltip title={tct('Error level: [level]', {level: capitalize(level)})}>
- <span />
- </Tooltip>
- </GroupLevel>
- )}
- {!hideIcons && status === 'ignored' && (
- <IconWrapper>
- <IconMute color="red300" />
- </IconWrapper>
- )}
- {!hideIcons && isBookmarked && (
- <IconWrapper>
- <IconStar isSolid color="yellow300" />
- </IconWrapper>
- )}
- <ErrorBoundary customComponent={<EventTitleError />} mini>
- <StyledEventOrGroupTitle
- data={data}
- organization={organization}
- hasSeen={hasGroupingTreeUI && hasSeen === undefined ? true : hasSeen}
- withStackTracePreview
- hasGuideAnchor={index === 0}
- guideAnchorName="issue_stream_title"
- />
- </ErrorBoundary>
- </Fragment>
- );
- }
- function getTitle() {
- const orgId = params?.orgId;
- const {id, status} = data as Group;
- const {eventID, groupID} = data as Event;
- const {location} = props;
- const commonEleProps = {
- 'data-test-id': status === 'resolved' ? 'resolved-issue' : null,
- style: status === 'resolved' ? {textDecoration: 'line-through'} : undefined,
- };
- if (includeLink) {
- return (
- <GlobalSelectionLink
- {...commonEleProps}
- to={{
- pathname: `/organizations/${orgId}/issues/${eventID ? groupID : id}/${
- eventID ? `events/${eventID}/` : ''
- }`,
- query: {
- query,
- ...(location.query.sort !== undefined ? {sort: location.query.sort} : {}), // This adds sort to the query if one was selected from the issues list page
- ...(location.query.project !== undefined ? {} : {_allp: 1}), // This appends _allp to the URL parameters if they have no project selected ("all" projects included in results). This is so that when we enter the issue details page and lock them to a project, we can properly take them back to the issue list page with no project selected (and not the locked project selected)
- },
- }}
- onClick={onClick}
- >
- {getTitleChildren()}
- </GlobalSelectionLink>
- );
- }
- return <span {...commonEleProps}>{getTitleChildren()}</span>;
- }
- const location = getLocation(data);
- const message = getMessage(data);
- return (
- <div className={className} data-test-id="event-issue-header">
- <Title size={size} hasGroupingTreeUI={hasGroupingTreeUI}>
- {getTitle()}
- </Title>
- {location && <Location size={size}>{location}</Location>}
- {message && (
- <StyledTagAndMessageWrapper size={size}>
- {message && <Message>{message}</Message>}
- </StyledTagAndMessageWrapper>
- )}
- </div>
- );
- }
- const truncateStyles = css`
- overflow: hidden;
- max-width: 100%;
- text-overflow: ellipsis;
- white-space: nowrap;
- `;
- const getMargin = ({size}: {size: Size}) => {
- if (size === 'small') {
- return 'margin: 0;';
- }
- return 'margin: 0 0 5px';
- };
- const Title = styled('div')<{size: Size; hasGroupingTreeUI: boolean}>`
- line-height: 1;
- ${getMargin};
- & em {
- font-size: ${p => p.theme.fontSizeMedium};
- font-style: normal;
- font-weight: 300;
- color: ${p => p.theme.subText};
- }
- ${p =>
- !p.hasGroupingTreeUI
- ? css`
- ${truncateStyles}
- `
- : css`
- > a:first-child {
- display: flex;
- }
- `}
- `;
- const LocationWrapper = styled('div')`
- ${truncateStyles};
- ${getMargin};
- direction: rtl;
- text-align: left;
- font-size: ${p => p.theme.fontSizeMedium};
- color: ${p => p.theme.subText};
- span {
- direction: ltr;
- }
- `;
- function Location(props) {
- const {children, ...rest} = props;
- return (
- <LocationWrapper {...rest}>
- {tct('in [location]', {
- location: <span>{children}</span>,
- })}
- </LocationWrapper>
- );
- }
- const StyledTagAndMessageWrapper = styled(TagAndMessageWrapper)`
- ${getMargin};
- `;
- const Message = styled('div')`
- ${truncateStyles};
- font-size: ${p => p.theme.fontSizeMedium};
- `;
- const IconWrapper = styled('span')`
- position: relative;
- top: 2px;
- margin-right: 5px;
- `;
- const GroupLevel = styled('div')<{level: Level}>`
- position: absolute;
- left: -1px;
- width: 9px;
- height: 15px;
- border-radius: 0 3px 3px 0;
- background-color: ${p => p.theme.level[p.level] ?? p.theme.level.default};
- & span {
- display: block;
- width: 9px;
- height: 15px;
- }
- `;
- export default withRouter(withOrganization(EventOrGroupHeader));
- const StyledEventOrGroupTitle = styled(EventOrGroupTitle)<{
- hasSeen: boolean;
- }>`
- font-weight: ${p => (p.hasSeen ? 400 : 600)};
- `;
|