sidebar.tsx 7.5 KB

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