sidebar.tsx 7.4 KB

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