sidebar.tsx 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
  1. import {Fragment, PureComponent} from 'react';
  2. import styled from '@emotion/styled';
  3. import ActorAvatar from 'sentry/components/avatar/actorAvatar';
  4. import {SectionHeading} from 'sentry/components/charts/styles';
  5. import {KeyValueTable, KeyValueTableRow} from 'sentry/components/keyValueTable';
  6. import {PanelBody} from 'sentry/components/panels';
  7. import TimeSince from 'sentry/components/timeSince';
  8. import {IconChevron} from 'sentry/icons';
  9. import {t, tct} from 'sentry/locale';
  10. import space from 'sentry/styles/space';
  11. import {Actor, Member, Team} from 'sentry/types';
  12. import {IssueAlertRule} from 'sentry/types/alerts';
  13. type Props = {
  14. memberList: Member[];
  15. rule: IssueAlertRule;
  16. teams: Team[];
  17. };
  18. class Sidebar extends PureComponent<Props> {
  19. renderConditions() {
  20. const {rule, memberList, teams} = this.props;
  21. const conditions = rule.conditions.length
  22. ? rule.conditions.map(condition => (
  23. <ConditionsBadge key={condition.id}>{condition.name}</ConditionsBadge>
  24. ))
  25. : null;
  26. const filters = rule.filters.length
  27. ? rule.filters.map(filter => (
  28. <ConditionsBadge key={filter.id}>
  29. {filter.time ? filter.name + '(s)' : filter.name}
  30. </ConditionsBadge>
  31. ))
  32. : null;
  33. const actions = rule.actions.length ? (
  34. rule.actions.map(action => {
  35. let name = action.name;
  36. if (action.targetType === 'Member') {
  37. const user = memberList.find(
  38. member => member.user.id === `${action.targetIdentifier}`
  39. );
  40. name = t('Send a notification to %s', user?.email);
  41. }
  42. if (action.targetType === 'Team') {
  43. const team = teams.find(tm => tm.id === `${action.targetIdentifier}`);
  44. name = t('Send a notification to #%s', team?.name);
  45. }
  46. if (
  47. action.id === 'sentry.integrations.slack.notify_action.SlackNotifyServiceAction'
  48. ) {
  49. // Remove (optionally, an ID: XXX) from slack action
  50. name = name.replace(/\(optionally.*\)/, '');
  51. // Remove tags if they aren't used
  52. name = name.replace(' and show tags [] in notification', '');
  53. }
  54. return <ConditionsBadge key={action.id}>{name}</ConditionsBadge>;
  55. })
  56. ) : (
  57. <ConditionsBadge>{t('Do nothing')}</ConditionsBadge>
  58. );
  59. return (
  60. <PanelBody>
  61. <Step>
  62. <StepContainer>
  63. <ChevronContainer>
  64. <IconChevron color="gray200" isCircled direction="right" size="sm" />
  65. </ChevronContainer>
  66. <StepContent>
  67. <StepLead>
  68. {tct('[when:When] an event is captured [selector]', {
  69. when: <Badge />,
  70. selector: conditions?.length ? t('and %s...', rule.actionMatch) : '',
  71. })}
  72. </StepLead>
  73. {conditions}
  74. </StepContent>
  75. </StepContainer>
  76. </Step>
  77. {filters && (
  78. <Step>
  79. <StepContainer>
  80. <ChevronContainer>
  81. <IconChevron color="gray200" isCircled direction="right" size="sm" />
  82. </ChevronContainer>
  83. <StepContent>
  84. <StepLead>
  85. {tct('[if:If] [selector] of these filters match', {
  86. if: <Badge />,
  87. selector: rule.filterMatch,
  88. })}
  89. </StepLead>
  90. {filters}
  91. </StepContent>
  92. </StepContainer>
  93. </Step>
  94. )}
  95. <Step>
  96. <StepContainer>
  97. <ChevronContainer>
  98. <IconChevron isCircled color="gray200" direction="right" size="sm" />
  99. </ChevronContainer>
  100. <div>
  101. <StepLead>
  102. {tct('[then:Then] perform these actions', {
  103. then: <Badge />,
  104. })}
  105. </StepLead>
  106. {actions}
  107. </div>
  108. </StepContainer>
  109. </Step>
  110. </PanelBody>
  111. );
  112. }
  113. render() {
  114. const {rule} = this.props;
  115. const ownerId = rule.owner?.split(':')[1];
  116. const teamActor = ownerId && {type: 'team' as Actor['type'], id: ownerId, name: ''};
  117. return (
  118. <Fragment>
  119. <StatusContainer>
  120. <HeaderItem>
  121. <Heading noMargin>{t('Last Triggered')}</Heading>
  122. <Status>
  123. {rule.lastTriggered ? (
  124. <TimeSince date={rule.lastTriggered} />
  125. ) : (
  126. t('No alerts triggered')
  127. )}
  128. </Status>
  129. </HeaderItem>
  130. </StatusContainer>
  131. <SidebarGroup>
  132. <Heading noMargin>{t('Alert Conditions')}</Heading>
  133. {this.renderConditions()}
  134. </SidebarGroup>
  135. <SidebarGroup>
  136. <Heading>{t('Alert Rule Details')}</Heading>
  137. <KeyValueTable>
  138. <KeyValueTableRow
  139. keyName={t('Environment')}
  140. value={<OverflowTableValue>{rule.environment ?? '-'}</OverflowTableValue>}
  141. />
  142. {rule.dateCreated && (
  143. <KeyValueTableRow
  144. keyName={t('Date Created')}
  145. value={<TimeSince date={rule.dateCreated} suffix={t('ago')} />}
  146. />
  147. )}
  148. {rule.createdBy && (
  149. <KeyValueTableRow
  150. keyName={t('Created By')}
  151. value={
  152. <OverflowTableValue>{rule.createdBy.name ?? '-'}</OverflowTableValue>
  153. }
  154. />
  155. )}
  156. <KeyValueTableRow
  157. keyName={t('Team')}
  158. value={
  159. teamActor ? <ActorAvatar actor={teamActor} size={24} /> : 'Unassigned'
  160. }
  161. />
  162. </KeyValueTable>
  163. </SidebarGroup>
  164. </Fragment>
  165. );
  166. }
  167. }
  168. export default Sidebar;
  169. const SidebarGroup = styled('div')`
  170. margin-bottom: ${space(3)};
  171. `;
  172. const HeaderItem = styled('div')`
  173. flex: 1;
  174. display: flex;
  175. flex-direction: column;
  176. > *:nth-child(2) {
  177. flex: 1;
  178. display: flex;
  179. align-items: center;
  180. }
  181. `;
  182. const Status = styled('div')`
  183. position: relative;
  184. display: grid;
  185. grid-template-columns: auto auto auto;
  186. gap: ${space(0.5)};
  187. font-size: ${p => p.theme.fontSizeLarge};
  188. `;
  189. const StatusContainer = styled('div')`
  190. height: 60px;
  191. display: flex;
  192. margin-bottom: ${space(1.5)};
  193. `;
  194. const Step = styled('div')`
  195. position: relative;
  196. margin-top: ${space(4)};
  197. :first-child {
  198. margin-top: ${space(1)};
  199. }
  200. `;
  201. const StepContainer = styled('div')`
  202. display: flex;
  203. align-items: flex-start;
  204. flex-grow: 1;
  205. `;
  206. const StepContent = styled('div')`
  207. &::before {
  208. content: '';
  209. position: absolute;
  210. height: 100%;
  211. top: 28px;
  212. left: ${space(1)};
  213. border-right: 1px ${p => p.theme.gray200} dashed;
  214. }
  215. `;
  216. const StepLead = styled('div')`
  217. margin-bottom: ${space(0.5)};
  218. font-size: ${p => p.theme.fontSizeMedium};
  219. font-weight: 400;
  220. `;
  221. const ChevronContainer = styled('div')`
  222. display: flex;
  223. align-items: center;
  224. padding: ${space(0.5)} ${space(1)} ${space(0.5)} 0;
  225. `;
  226. const Badge = styled('span')`
  227. display: inline-block;
  228. background-color: ${p => p.theme.purple300};
  229. padding: 0 ${space(0.75)};
  230. border-radius: ${p => p.theme.borderRadius};
  231. color: ${p => p.theme.white};
  232. text-transform: uppercase;
  233. text-align: center;
  234. font-size: ${p => p.theme.fontSizeSmall};
  235. font-weight: 400;
  236. line-height: 1.5;
  237. `;
  238. const ConditionsBadge = styled('span')`
  239. display: block;
  240. background-color: ${p => p.theme.surface100};
  241. padding: 0 ${space(0.75)};
  242. border-radius: ${p => p.theme.borderRadius};
  243. color: ${p => p.theme.textColor};
  244. font-size: ${p => p.theme.fontSizeSmall};
  245. margin-bottom: ${space(1)};
  246. width: fit-content;
  247. font-weight: 400;
  248. `;
  249. const Heading = styled(SectionHeading)<{noMargin?: boolean}>`
  250. margin-top: ${p => (p.noMargin ? 0 : space(2))};
  251. margin-bottom: ${p => (p.noMargin ? 0 : space(1))};
  252. `;
  253. const OverflowTableValue = styled('div')`
  254. ${p => p.theme.overflowEllipsis}
  255. `;