sidebar.tsx 7.1 KB

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