teamAlertsTriggered.tsx 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. import {Fragment} from 'react';
  2. import {css} from '@emotion/react';
  3. import styled from '@emotion/styled';
  4. import round from 'lodash/round';
  5. import AsyncComponent from 'sentry/components/asyncComponent';
  6. import Button from 'sentry/components/button';
  7. import {BarChart} from 'sentry/components/charts/barChart';
  8. import {DateTimeObject} from 'sentry/components/charts/utils';
  9. import Link from 'sentry/components/links/link';
  10. import LoadingIndicator from 'sentry/components/loadingIndicator';
  11. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  12. import PanelTable from 'sentry/components/panels/panelTable';
  13. import {IconArrow} from 'sentry/icons';
  14. import {t, tct} from 'sentry/locale';
  15. import space from 'sentry/styles/space';
  16. import {Organization, Project} from 'sentry/types';
  17. import {formatPercentage} from 'sentry/utils/formatters';
  18. import {Color} from 'sentry/utils/theme';
  19. import {MetricRule} from 'sentry/views/alerts/rules/metric/types';
  20. import {ProjectBadge, ProjectBadgeContainer} from './styles';
  21. import {barAxisLabel, convertDayValueObjectToSeries, sortSeriesByDay} from './utils';
  22. type AlertsTriggered = Record<string, number>;
  23. type AlertsTriggeredRule = MetricRule & {
  24. totalThisWeek: number;
  25. weeklyAvg: number;
  26. };
  27. type Props = AsyncComponent['props'] & {
  28. organization: Organization;
  29. projects: Project[];
  30. teamSlug: string;
  31. } & DateTimeObject;
  32. type State = AsyncComponent['state'] & {
  33. alertsTriggered: AlertsTriggered | null;
  34. alertsTriggeredRules: AlertsTriggeredRule[] | null;
  35. };
  36. class TeamAlertsTriggered extends AsyncComponent<Props, State> {
  37. shouldRenderBadRequests = true;
  38. getDefaultState(): State {
  39. return {
  40. ...super.getDefaultState(),
  41. alertsTriggered: null,
  42. alertsTriggeredRules: null,
  43. };
  44. }
  45. getEndpoints(): ReturnType<AsyncComponent['getEndpoints']> {
  46. const {organization, start, end, period, utc, teamSlug} = this.props;
  47. const datetime = {start, end, period, utc};
  48. return [
  49. [
  50. 'alertsTriggered',
  51. `/teams/${organization.slug}/${teamSlug}/alerts-triggered/`,
  52. {
  53. query: {
  54. ...normalizeDateTimeParams(datetime),
  55. },
  56. },
  57. ],
  58. [
  59. 'alertsTriggeredRules',
  60. `/teams/${organization.slug}/${teamSlug}/alerts-triggered-index/`,
  61. {
  62. query: {
  63. ...normalizeDateTimeParams(datetime),
  64. },
  65. },
  66. ],
  67. ];
  68. }
  69. componentDidUpdate(prevProps: Props) {
  70. const {start, end, period, utc, teamSlug} = this.props;
  71. if (
  72. prevProps.start !== start ||
  73. prevProps.end !== end ||
  74. prevProps.period !== period ||
  75. prevProps.utc !== utc ||
  76. prevProps.teamSlug !== teamSlug
  77. ) {
  78. this.remountComponent();
  79. }
  80. }
  81. renderTrend(rule: AlertsTriggeredRule) {
  82. const {weeklyAvg, totalThisWeek} = rule;
  83. const diff = totalThisWeek - weeklyAvg;
  84. // weeklyAvg can only be 0 only if totalThisWeek is also 0
  85. // but those should never be returned in alerts-triggered-index request
  86. if (weeklyAvg === 0) {
  87. return '\u2014';
  88. }
  89. return (
  90. <SubText color={diff <= 0 ? 'green300' : 'red300'}>
  91. {formatPercentage(Math.abs(diff / weeklyAvg), 0)}
  92. <PaddedIconArrow direction={diff <= 0 ? 'down' : 'up'} size="xs" />
  93. </SubText>
  94. );
  95. }
  96. renderLoading() {
  97. return (
  98. <ChartWrapper>
  99. <LoadingIndicator />
  100. </ChartWrapper>
  101. );
  102. }
  103. renderBody() {
  104. const {organization, period, projects} = this.props;
  105. const {alertsTriggered, alertsTriggeredRules} = this.state;
  106. const seriesData = sortSeriesByDay(
  107. convertDayValueObjectToSeries(alertsTriggered ?? {})
  108. );
  109. return (
  110. <Fragment>
  111. <ChartWrapper>
  112. <BarChart
  113. style={{height: 190}}
  114. isGroupedByDate
  115. useShortDate
  116. period="7d"
  117. legend={{right: 0, top: 0}}
  118. yAxis={{minInterval: 1}}
  119. xAxis={barAxisLabel(seriesData.length)}
  120. series={[
  121. {
  122. seriesName: t('Alerts Triggered'),
  123. data: seriesData,
  124. silent: true,
  125. barCategoryGap: '5%',
  126. },
  127. ]}
  128. />
  129. </ChartWrapper>
  130. <StyledPanelTable
  131. isEmpty={
  132. !alertsTriggered || !alertsTriggeredRules || alertsTriggeredRules.length === 0
  133. }
  134. emptyMessage={t('No alerts triggered for this team’s projects')}
  135. emptyAction={
  136. <ButtonsContainer>
  137. <Button
  138. priority="primary"
  139. size="sm"
  140. to={`/organizations/${organization.slug}/alerts/rules/`}
  141. >
  142. {t('Create Alert')}
  143. </Button>
  144. <Button
  145. size="sm"
  146. external
  147. to="https://docs.sentry.io/product/alerts/create-alerts/"
  148. >
  149. {t('Learn more')}
  150. </Button>
  151. </ButtonsContainer>
  152. }
  153. headers={[
  154. t('Alert Rule'),
  155. t('Project'),
  156. <AlignRight key="last">{tct('Last [period] Average', {period})}</AlignRight>,
  157. <AlignRight key="curr">{t('This Week')}</AlignRight>,
  158. <AlignRight key="diff">{t('Difference')}</AlignRight>,
  159. ]}
  160. >
  161. {alertsTriggeredRules?.map(rule => {
  162. const project = projects.find(p => p.slug === rule.projects[0]);
  163. return (
  164. <Fragment key={rule.id}>
  165. <AlertNameContainer>
  166. <Link
  167. to={`/organizations/${organization.slug}/alerts/rules/details/${rule.id}/`}
  168. >
  169. {rule.name}
  170. </Link>
  171. </AlertNameContainer>
  172. <ProjectBadgeContainer>
  173. {project && <ProjectBadge avatarSize={18} project={project} />}
  174. </ProjectBadgeContainer>
  175. <AlignRight>{round(rule.weeklyAvg, 2)}</AlignRight>
  176. <AlignRight>{rule.totalThisWeek}</AlignRight>
  177. <AlignRight>{this.renderTrend(rule)}</AlignRight>
  178. </Fragment>
  179. );
  180. })}
  181. </StyledPanelTable>
  182. </Fragment>
  183. );
  184. }
  185. }
  186. export default TeamAlertsTriggered;
  187. const ChartWrapper = styled('div')`
  188. padding: ${space(2)} ${space(2)} 0 ${space(2)};
  189. border-bottom: 1px solid ${p => p.theme.border};
  190. `;
  191. const StyledPanelTable = styled(PanelTable)`
  192. grid-template-columns: 1fr 0.5fr 0.2fr 0.2fr 0.2fr;
  193. font-size: ${p => p.theme.fontSizeMedium};
  194. white-space: nowrap;
  195. margin-bottom: 0;
  196. border: 0;
  197. box-shadow: unset;
  198. & > div {
  199. padding: ${space(1)} ${space(2)};
  200. }
  201. ${p =>
  202. p.isEmpty &&
  203. css`
  204. & > div:last-child {
  205. padding: 48px ${space(2)};
  206. }
  207. `}
  208. `;
  209. const AlertNameContainer = styled('div')`
  210. ${p => p.theme.overflowEllipsis}
  211. `;
  212. const AlignRight = styled('div')`
  213. text-align: right;
  214. font-variant-numeric: tabular-nums;
  215. `;
  216. const PaddedIconArrow = styled(IconArrow)`
  217. margin: 0 ${space(0.5)};
  218. `;
  219. const SubText = styled('div')<{color: Color}>`
  220. color: ${p => p.theme[p.color]};
  221. `;
  222. const ButtonsContainer = styled('div')`
  223. & > a {
  224. margin-right: ${space(0.5)};
  225. margin-left: ${space(0.5)};
  226. }
  227. `;