row.tsx 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. import {Fragment, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import moment from 'moment';
  4. import ActorAvatar from 'sentry/components/avatar/actorAvatar';
  5. import Duration from 'sentry/components/duration';
  6. import ErrorBoundary from 'sentry/components/errorBoundary';
  7. import IdBadge from 'sentry/components/idBadge';
  8. import Link from 'sentry/components/links/link';
  9. import {Tag} from 'sentry/components/tag';
  10. import TimeSince from 'sentry/components/timeSince';
  11. import {t} from 'sentry/locale';
  12. import TeamStore from 'sentry/stores/teamStore';
  13. import {space} from 'sentry/styles/space';
  14. import type {Actor, Organization, Project} from 'sentry/types';
  15. import getDynamicText from 'sentry/utils/getDynamicText';
  16. import type {Incident} from 'sentry/views/alerts/types';
  17. import {IncidentStatus} from 'sentry/views/alerts/types';
  18. import {alertDetailsLink} from 'sentry/views/alerts/utils';
  19. type Props = {
  20. incident: Incident;
  21. organization: Organization;
  22. projects: Project[];
  23. projectsLoaded: boolean;
  24. };
  25. function AlertListRow({incident, projectsLoaded, projects, organization}: Props) {
  26. const slug = incident.projects[0];
  27. const started = moment(incident.dateStarted);
  28. const duration = moment
  29. .duration(moment(incident.dateClosed || new Date()).diff(started))
  30. .as('seconds');
  31. const project = useMemo(() => projects.find(p => p.slug === slug), [slug, projects]);
  32. const alertLink = {
  33. pathname: alertDetailsLink(organization, incident),
  34. query: {alert: incident.identifier},
  35. };
  36. const ownerId = incident.alertRule.owner?.split(':')[1];
  37. let teamName = '';
  38. if (ownerId) {
  39. teamName = TeamStore.getById(ownerId)?.name ?? '';
  40. }
  41. const teamActor = ownerId
  42. ? {type: 'team' as Actor['type'], id: ownerId, name: teamName}
  43. : null;
  44. return (
  45. <ErrorBoundary>
  46. <FlexCenter>
  47. <Title data-test-id="alert-title">
  48. <Link to={alertLink}>{incident.title}</Link>
  49. </Title>
  50. </FlexCenter>
  51. <NoWrapNumeric>
  52. {getDynamicText({
  53. value: <TimeSince date={incident.dateStarted} unitStyle="extraShort" />,
  54. fixed: '1w ago',
  55. })}
  56. </NoWrapNumeric>
  57. <NoWrapNumeric>
  58. {incident.status === IncidentStatus.CLOSED ? (
  59. <Duration seconds={getDynamicText({value: duration, fixed: 1200})} />
  60. ) : (
  61. <Tag type="warning">{t('Still Active')}</Tag>
  62. )}
  63. </NoWrapNumeric>
  64. <FlexCenter>
  65. <ProjectBadge avatarSize={18} project={!projectsLoaded ? {slug} : project} />
  66. </FlexCenter>
  67. <NoWrapNumeric>#{incident.id}</NoWrapNumeric>
  68. <FlexCenter>
  69. {teamActor ? (
  70. <Fragment>
  71. <StyledActorAvatar actor={teamActor} size={18} hasTooltip={false} />{' '}
  72. <TeamWrapper>{teamActor.name}</TeamWrapper>
  73. </Fragment>
  74. ) : (
  75. '-'
  76. )}
  77. </FlexCenter>
  78. </ErrorBoundary>
  79. );
  80. }
  81. const Title = styled('div')`
  82. ${p => p.theme.overflowEllipsis}
  83. min-width: 130px;
  84. `;
  85. const ProjectBadge = styled(IdBadge)`
  86. flex-shrink: 0;
  87. `;
  88. const FlexCenter = styled('div')`
  89. ${p => p.theme.overflowEllipsis}
  90. display: flex;
  91. align-items: center;
  92. line-height: 1.6;
  93. `;
  94. const NoWrapNumeric = styled(FlexCenter)`
  95. white-space: nowrap;
  96. font-variant-numeric: tabular-nums;
  97. `;
  98. const TeamWrapper = styled('span')`
  99. ${p => p.theme.overflowEllipsis}
  100. `;
  101. const StyledActorAvatar = styled(ActorAvatar)`
  102. margin-right: ${space(1)};
  103. `;
  104. export default AlertListRow;