body.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. import {Component} from 'react';
  2. import {RouteComponentProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import Feature from 'app/components/acl/feature';
  5. import Alert from 'app/components/alert';
  6. import Button from 'app/components/button';
  7. import {SectionHeading} from 'app/components/charts/styles';
  8. import Duration from 'app/components/duration';
  9. import {KeyValueTable, KeyValueTableRow} from 'app/components/keyValueTable';
  10. import Link from 'app/components/links/link';
  11. import NavTabs from 'app/components/navTabs';
  12. import {Panel, PanelBody, PanelFooter} from 'app/components/panels';
  13. import Placeholder from 'app/components/placeholder';
  14. import SeenByList from 'app/components/seenByList';
  15. import {IconWarning} from 'app/icons';
  16. import {t, tct} from 'app/locale';
  17. import {PageContent} from 'app/styles/organization';
  18. import space from 'app/styles/space';
  19. import {Organization, Project} from 'app/types';
  20. import {defined} from 'app/utils';
  21. import Projects from 'app/utils/projects';
  22. import theme from 'app/utils/theme';
  23. import {alertDetailsLink} from 'app/views/alerts/details/index';
  24. import {DATASET_EVENT_TYPE_FILTERS} from 'app/views/alerts/incidentRules/constants';
  25. import {makeDefaultCta} from 'app/views/alerts/incidentRules/presets';
  26. import {AlertRuleThresholdType} from 'app/views/alerts/incidentRules/types';
  27. import {
  28. AlertRuleStatus,
  29. Incident,
  30. IncidentStats,
  31. IncidentStatus,
  32. IncidentStatusMethod,
  33. } from '../types';
  34. import {DATA_SOURCE_LABELS, getIncidentMetricPreset, isIssueAlert} from '../utils';
  35. import Activity from './activity';
  36. import Chart from './chart';
  37. type Props = {
  38. incident?: Incident;
  39. organization: Organization;
  40. stats?: IncidentStats;
  41. } & RouteComponentProps<{alertId: string; orgId: string}, {}>;
  42. export default class DetailsBody extends Component<Props> {
  43. get metricPreset() {
  44. const {incident} = this.props;
  45. return incident ? getIncidentMetricPreset(incident) : undefined;
  46. }
  47. /**
  48. * Return a string describing the threshold based on the threshold and the type
  49. */
  50. getThresholdText(
  51. value: number | '' | null | undefined,
  52. thresholdType: AlertRuleThresholdType,
  53. isAlert: boolean = false
  54. ) {
  55. if (!defined(value)) {
  56. return '';
  57. }
  58. const isAbove = thresholdType === AlertRuleThresholdType.ABOVE;
  59. const direction = isAbove === isAlert ? '>' : '<';
  60. return `${direction} ${value}`;
  61. }
  62. renderRuleDetails() {
  63. const {incident} = this.props;
  64. if (incident === undefined) {
  65. return <Placeholder height="200px" />;
  66. }
  67. const criticalTrigger = incident?.alertRule.triggers.find(
  68. ({label}) => label === 'critical'
  69. );
  70. const warningTrigger = incident?.alertRule.triggers.find(
  71. ({label}) => label === 'warning'
  72. );
  73. return (
  74. <KeyValueTable>
  75. <KeyValueTableRow
  76. keyName={t('Data Source')}
  77. value={DATA_SOURCE_LABELS[incident.alertRule?.dataset]}
  78. />
  79. <KeyValueTableRow keyName={t('Metric')} value={incident.alertRule?.aggregate} />
  80. <KeyValueTableRow
  81. keyName={t('Time Window')}
  82. value={incident && <Duration seconds={incident.alertRule.timeWindow * 60} />}
  83. />
  84. {incident.alertRule?.query && (
  85. <KeyValueTableRow
  86. keyName={t('Filter')}
  87. value={
  88. <span title={incident.alertRule?.query}>{incident.alertRule?.query}</span>
  89. }
  90. />
  91. )}
  92. <KeyValueTableRow
  93. keyName={t('Critical Trigger')}
  94. value={this.getThresholdText(
  95. criticalTrigger?.alertThreshold,
  96. incident.alertRule?.thresholdType,
  97. true
  98. )}
  99. />
  100. {defined(warningTrigger) && (
  101. <KeyValueTableRow
  102. keyName={t('Warning Trigger')}
  103. value={this.getThresholdText(
  104. warningTrigger?.alertThreshold,
  105. incident.alertRule?.thresholdType,
  106. true
  107. )}
  108. />
  109. )}
  110. {defined(incident.alertRule?.resolveThreshold) && (
  111. <KeyValueTableRow
  112. keyName={t('Resolution')}
  113. value={this.getThresholdText(
  114. incident.alertRule?.resolveThreshold,
  115. incident.alertRule?.thresholdType
  116. )}
  117. />
  118. )}
  119. </KeyValueTable>
  120. );
  121. }
  122. renderChartHeader() {
  123. const {incident} = this.props;
  124. const alertRule = incident?.alertRule;
  125. return (
  126. <ChartHeader>
  127. <div>
  128. {this.metricPreset?.name ?? t('Custom metric')}
  129. <ChartParameters>
  130. {tct('Metric: [metric] over [window]', {
  131. metric: <code>{alertRule?.aggregate ?? '\u2026'}</code>,
  132. window: (
  133. <code>
  134. {incident ? (
  135. <Duration seconds={incident.alertRule.timeWindow * 60} />
  136. ) : (
  137. '\u2026'
  138. )}
  139. </code>
  140. ),
  141. })}
  142. {(alertRule?.query || incident?.alertRule?.dataset) &&
  143. tct('Filter: [datasetType] [filter]', {
  144. datasetType: incident?.alertRule?.dataset && (
  145. <code>{DATASET_EVENT_TYPE_FILTERS[incident.alertRule.dataset]}</code>
  146. ),
  147. filter: alertRule?.query && <code>{alertRule.query}</code>,
  148. })}
  149. </ChartParameters>
  150. </div>
  151. </ChartHeader>
  152. );
  153. }
  154. renderChartActions() {
  155. const {incident, params, stats} = this.props;
  156. return (
  157. // Currently only one button in pannel, hide panel if not available
  158. <Feature features={['discover-basic']}>
  159. <ChartActions>
  160. <Projects slugs={incident?.projects} orgId={params.orgId}>
  161. {({initiallyLoaded, fetching, projects}) => {
  162. const preset = this.metricPreset;
  163. const ctaOpts = {
  164. orgSlug: params.orgId,
  165. projects: (initiallyLoaded ? projects : []) as Project[],
  166. incident,
  167. stats,
  168. };
  169. const {buttonText, ...props} = preset
  170. ? preset.makeCtaParams(ctaOpts)
  171. : makeDefaultCta(ctaOpts);
  172. return (
  173. <Button
  174. size="small"
  175. priority="primary"
  176. disabled={!incident || fetching || !initiallyLoaded}
  177. {...props}
  178. >
  179. {buttonText}
  180. </Button>
  181. );
  182. }}
  183. </Projects>
  184. </ChartActions>
  185. </Feature>
  186. );
  187. }
  188. render() {
  189. const {params, incident, organization, stats} = this.props;
  190. const hasRedesign =
  191. incident?.alertRule &&
  192. !isIssueAlert(incident?.alertRule) &&
  193. organization.features.includes('alert-details-redesign');
  194. const alertRuleLink =
  195. hasRedesign && incident
  196. ? alertDetailsLink(organization, incident)
  197. : `/organizations/${params.orgId}/alerts/metric-rules/${incident?.projects[0]}/${incident?.alertRule?.id}/`;
  198. return (
  199. <StyledPageContent>
  200. <Main>
  201. {incident &&
  202. incident.status === IncidentStatus.CLOSED &&
  203. incident.statusMethod === IncidentStatusMethod.RULE_UPDATED && (
  204. <AlertWrapper>
  205. <Alert type="warning" icon={<IconWarning size="sm" />}>
  206. {t(
  207. 'This alert has been auto-resolved because the rule that triggered it has been modified or deleted'
  208. )}
  209. </Alert>
  210. </AlertWrapper>
  211. )}
  212. <PageContent>
  213. <ChartPanel>
  214. <PanelBody withPadding>
  215. {this.renderChartHeader()}
  216. {incident && stats ? (
  217. <Chart
  218. triggers={incident.alertRule.triggers}
  219. resolveThreshold={incident.alertRule.resolveThreshold}
  220. aggregate={incident.alertRule.aggregate}
  221. data={stats.eventStats.data}
  222. started={incident.dateStarted}
  223. closed={incident.dateClosed || undefined}
  224. />
  225. ) : (
  226. <Placeholder height="200px" />
  227. )}
  228. </PanelBody>
  229. {this.renderChartActions()}
  230. </ChartPanel>
  231. </PageContent>
  232. <DetailWrapper>
  233. <ActivityPageContent>
  234. <StyledNavTabs underlined>
  235. <li className="active">
  236. <Link to="">{t('Activity')}</Link>
  237. </li>
  238. <SeenByTab>
  239. {incident && (
  240. <StyledSeenByList
  241. iconPosition="right"
  242. seenBy={incident.seenBy}
  243. iconTooltip={t('People who have viewed this alert')}
  244. />
  245. )}
  246. </SeenByTab>
  247. </StyledNavTabs>
  248. <Activity
  249. incident={incident}
  250. params={params}
  251. incidentStatus={!!incident ? incident.status : null}
  252. />
  253. </ActivityPageContent>
  254. <Sidebar>
  255. <SidebarHeading>
  256. <span>{t('Alert Rule')}</span>
  257. {(incident?.alertRule?.status !== AlertRuleStatus.SNAPSHOT ||
  258. hasRedesign) && (
  259. <SideHeaderLink
  260. disabled={!!incident?.id}
  261. to={
  262. incident?.id
  263. ? {
  264. pathname: alertRuleLink,
  265. }
  266. : ''
  267. }
  268. >
  269. {t('View Alert Rule')}
  270. </SideHeaderLink>
  271. )}
  272. </SidebarHeading>
  273. {this.renderRuleDetails()}
  274. </Sidebar>
  275. </DetailWrapper>
  276. </Main>
  277. </StyledPageContent>
  278. );
  279. }
  280. }
  281. const Main = styled('div')`
  282. background-color: ${p => p.theme.background};
  283. padding-top: ${space(3)};
  284. flex-grow: 1;
  285. `;
  286. const DetailWrapper = styled('div')`
  287. display: flex;
  288. flex: 1;
  289. @media (max-width: ${p => p.theme.breakpoints[0]}) {
  290. flex-direction: column-reverse;
  291. }
  292. `;
  293. const ActivityPageContent = styled(PageContent)`
  294. @media (max-width: ${theme.breakpoints[0]}) {
  295. width: 100%;
  296. margin-bottom: 0;
  297. }
  298. `;
  299. const Sidebar = styled(PageContent)`
  300. width: 400px;
  301. flex: none;
  302. padding-top: ${space(3)};
  303. @media (max-width: ${theme.breakpoints[0]}) {
  304. width: 100%;
  305. padding-top: ${space(3)};
  306. margin-bottom: 0;
  307. border-bottom: 1px solid ${p => p.theme.border};
  308. }
  309. `;
  310. const SidebarHeading = styled(SectionHeading)`
  311. display: flex;
  312. justify-content: space-between;
  313. `;
  314. const SideHeaderLink = styled(Link)`
  315. font-weight: normal;
  316. `;
  317. const StyledPageContent = styled(PageContent)`
  318. padding: 0;
  319. flex-direction: column;
  320. `;
  321. const ChartPanel = styled(Panel)``;
  322. const ChartHeader = styled('header')`
  323. margin-bottom: ${space(1)};
  324. `;
  325. const ChartActions = styled(PanelFooter)`
  326. display: flex;
  327. justify-content: flex-end;
  328. padding: ${space(2)};
  329. `;
  330. const ChartParameters = styled('div')`
  331. color: ${p => p.theme.subText};
  332. font-size: ${p => p.theme.fontSizeMedium};
  333. display: grid;
  334. grid-auto-flow: column;
  335. grid-auto-columns: max-content;
  336. grid-gap: ${space(4)};
  337. align-items: center;
  338. overflow-x: auto;
  339. > * {
  340. position: relative;
  341. }
  342. > *:not(:last-of-type):after {
  343. content: '';
  344. display: block;
  345. height: 70%;
  346. width: 1px;
  347. background: ${p => p.theme.gray200};
  348. position: absolute;
  349. right: -${space(2)};
  350. top: 15%;
  351. }
  352. `;
  353. const AlertWrapper = styled('div')`
  354. padding: ${space(2)} ${space(4)} 0;
  355. `;
  356. const StyledNavTabs = styled(NavTabs)`
  357. display: flex;
  358. `;
  359. const SeenByTab = styled('li')`
  360. flex: 1;
  361. margin-left: ${space(2)};
  362. margin-right: 0;
  363. .nav-tabs > & {
  364. margin-right: 0;
  365. }
  366. `;
  367. const StyledSeenByList = styled(SeenByList)`
  368. margin-top: 0;
  369. `;