sidebar.tsx 6.9 KB

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