linkedIssue.tsx 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. import styled from '@emotion/styled';
  2. import {Alert} from 'sentry/components/alert';
  3. import SeenByList from 'sentry/components/avatar/seenByList';
  4. import {SectionHeading} from 'sentry/components/charts/styles';
  5. import Times from 'sentry/components/group/times';
  6. import ProjectBadge from 'sentry/components/idBadge/projectBadge';
  7. import Link from 'sentry/components/links/link';
  8. import LoadingError from 'sentry/components/loadingError';
  9. import Placeholder from 'sentry/components/placeholder';
  10. import ShortId from 'sentry/components/shortId';
  11. import GroupChart from 'sentry/components/stream/groupChart';
  12. import {t} from 'sentry/locale';
  13. import {space} from 'sentry/styles/space';
  14. import type {TimeseriesValue} from 'sentry/types';
  15. import type {Group} from 'sentry/types/group';
  16. import {useApiQuery} from 'sentry/utils/queryClient';
  17. type Props = {
  18. eventId: string;
  19. groupId: string;
  20. };
  21. function LinkedIssue({eventId, groupId}: Props) {
  22. const groupUrl = `/issues/${groupId}/`;
  23. const {
  24. data: group,
  25. isLoading,
  26. isError,
  27. error,
  28. } = useApiQuery<Group>([groupUrl], {staleTime: 0});
  29. if (isLoading) {
  30. return <Placeholder height="120px" bottomGutter={2} />;
  31. }
  32. if (isError || !group) {
  33. const hasNotFound = error.status === 404;
  34. if (hasNotFound) {
  35. return (
  36. <Alert type="warning" showIcon>
  37. {t('The linked issue cannot be found. It may have been deleted, or merged.')}
  38. </Alert>
  39. );
  40. }
  41. return <LoadingError />;
  42. }
  43. const issueUrl = `${group.permalink}events/${eventId}/`;
  44. const groupStats: ReadonlyArray<TimeseriesValue> = group.filtered
  45. ? group.filtered.stats?.['30d']
  46. : group.stats?.['30d'];
  47. const groupSecondaryStats: ReadonlyArray<TimeseriesValue> = group.filtered
  48. ? group.stats?.['30d']
  49. : [];
  50. return (
  51. <Section>
  52. <SectionHeading>{t('Event Issue')}</SectionHeading>
  53. <StyledIssueCard>
  54. <IssueCardHeader>
  55. <StyledLink to={issueUrl} data-test-id="linked-issue">
  56. <StyledShortId
  57. shortId={group.shortId}
  58. avatar={
  59. <ProjectBadge
  60. project={group.project}
  61. avatarSize={16}
  62. hideName
  63. disableLink
  64. />
  65. }
  66. />
  67. </StyledLink>
  68. <SeenByList seenBy={group.seenBy} maxVisibleAvatars={5} />
  69. </IssueCardHeader>
  70. <IssueCardBody>
  71. <GroupChart
  72. stats={groupStats}
  73. secondaryStats={groupSecondaryStats}
  74. height={56}
  75. />
  76. </IssueCardBody>
  77. <IssueCardFooter>
  78. <Times lastSeen={group.lastSeen} firstSeen={group.firstSeen} />
  79. </IssueCardFooter>
  80. </StyledIssueCard>
  81. </Section>
  82. );
  83. }
  84. const Section = styled('div')`
  85. margin-bottom: ${space(4)};
  86. `;
  87. const StyledIssueCard = styled('div')`
  88. border: 1px solid ${p => p.theme.border};
  89. border-radius: ${p => p.theme.borderRadius};
  90. `;
  91. const IssueCardHeader = styled('div')`
  92. display: flex;
  93. align-items: center;
  94. justify-content: space-between;
  95. padding: ${space(1)};
  96. `;
  97. const StyledLink = styled(Link)`
  98. justify-content: flex-start;
  99. `;
  100. const IssueCardBody = styled('div')`
  101. background: ${p => p.theme.backgroundSecondary};
  102. padding-top: ${space(1)};
  103. `;
  104. const StyledShortId = styled(ShortId)`
  105. font-size: ${p => p.theme.fontSizeMedium};
  106. color: ${p => p.theme.textColor};
  107. `;
  108. const IssueCardFooter = styled('div')`
  109. color: ${p => p.theme.gray300};
  110. font-size: ${p => p.theme.fontSizeSmall};
  111. padding: ${space(0.5)} ${space(1)};
  112. `;
  113. export default LinkedIssue;