createAlertButton.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. // eslint-disable-next-line no-restricted-imports
  2. import {withRouter, WithRouterProps} from 'react-router';
  3. import {
  4. addErrorMessage,
  5. addLoadingMessage,
  6. addSuccessMessage,
  7. } from 'sentry/actionCreators/indicator';
  8. import {navigateTo} from 'sentry/actionCreators/navigation';
  9. import Access from 'sentry/components/acl/access';
  10. import GuideAnchor from 'sentry/components/assistant/guideAnchor';
  11. import Button, {ButtonProps} from 'sentry/components/button';
  12. import Link from 'sentry/components/links/link';
  13. import {IconSiren} from 'sentry/icons';
  14. import type {SVGIconProps} from 'sentry/icons/svgIcon';
  15. import {t, tct} from 'sentry/locale';
  16. import type {Organization, Project} from 'sentry/types';
  17. import type EventView from 'sentry/utils/discover/eventView';
  18. import useApi from 'sentry/utils/useApi';
  19. import {
  20. AlertType,
  21. AlertWizardAlertNames,
  22. AlertWizardRuleTemplates,
  23. DEFAULT_WIZARD_TEMPLATE,
  24. } from 'sentry/views/alerts/wizard/options';
  25. export type CreateAlertFromViewButtonProps = ButtonProps & {
  26. /**
  27. * Discover query used to create the alert
  28. */
  29. eventView: EventView;
  30. organization: Organization;
  31. projects: Project[];
  32. alertType?: AlertType;
  33. className?: string;
  34. /**
  35. * Passed in value to override any metrics decision and switch back to transactions dataset.
  36. * We currently do a few checks on metrics data on performance pages and this passes the decision onward to alerts.
  37. */
  38. disableMetricDataset?: boolean;
  39. /**
  40. * Called when the user is redirected to the alert builder
  41. */
  42. onClick?: () => void;
  43. referrer?: string;
  44. };
  45. /**
  46. * Provide a button that can create an alert from an event view.
  47. * Emits incompatible query issues on click
  48. */
  49. function CreateAlertFromViewButton({
  50. projects,
  51. eventView,
  52. organization,
  53. referrer,
  54. onClick,
  55. alertType,
  56. disableMetricDataset,
  57. ...buttonProps
  58. }: CreateAlertFromViewButtonProps) {
  59. const project = projects.find(p => p.id === `${eventView.project[0]}`);
  60. const queryParams = eventView.generateQueryStringObject();
  61. if (queryParams.query?.includes(`project:${project?.slug}`)) {
  62. queryParams.query = (queryParams.query as string).replace(
  63. `project:${project?.slug}`,
  64. ''
  65. );
  66. }
  67. const alertTemplate = alertType
  68. ? AlertWizardRuleTemplates[alertType]
  69. : DEFAULT_WIZARD_TEMPLATE;
  70. const to = {
  71. pathname: `/organizations/${organization.slug}/alerts/new/metric/`,
  72. query: {
  73. ...queryParams,
  74. createFromDiscover: true,
  75. disableMetricDataset,
  76. referrer,
  77. ...alertTemplate,
  78. project: project?.slug,
  79. aggregate: queryParams.yAxis ?? alertTemplate.aggregate,
  80. },
  81. };
  82. const handleClick = () => {
  83. onClick?.();
  84. };
  85. return (
  86. <CreateAlertButton
  87. organization={organization}
  88. onClick={handleClick}
  89. to={to}
  90. aria-label={t('Create Alert')}
  91. {...buttonProps}
  92. />
  93. );
  94. }
  95. type CreateAlertButtonProps = {
  96. organization: Organization;
  97. alertOption?: keyof typeof AlertWizardAlertNames;
  98. hideIcon?: boolean;
  99. iconProps?: SVGIconProps;
  100. /**
  101. * Callback when the button is clicked.
  102. * This is different from `onClick` which always overrides the default
  103. * behavior when the button was clicked.
  104. */
  105. onEnter?: () => void;
  106. projectSlug?: string;
  107. referrer?: string;
  108. showPermissionGuide?: boolean;
  109. } & WithRouterProps &
  110. ButtonProps;
  111. const CreateAlertButton = withRouter(
  112. ({
  113. organization,
  114. projectSlug,
  115. iconProps,
  116. referrer,
  117. router,
  118. hideIcon,
  119. showPermissionGuide,
  120. alertOption,
  121. onEnter,
  122. ...buttonProps
  123. }: CreateAlertButtonProps) => {
  124. const api = useApi();
  125. const createAlertUrl = (providedProj: string): string => {
  126. const params = new URLSearchParams();
  127. if (referrer) {
  128. params.append('referrer', referrer);
  129. }
  130. if (providedProj !== ':projectId') {
  131. params.append('project', providedProj);
  132. }
  133. if (alertOption) {
  134. params.append('alert_option', alertOption);
  135. }
  136. return `/organizations/${organization.slug}/alerts/wizard/?${params.toString()}`;
  137. };
  138. function handleClickWithoutProject(event: React.MouseEvent) {
  139. event.preventDefault();
  140. onEnter?.();
  141. navigateTo(createAlertUrl(':projectId'), router);
  142. }
  143. async function enableAlertsMemberWrite() {
  144. const settingsEndpoint = `/organizations/${organization.slug}/`;
  145. addLoadingMessage();
  146. try {
  147. await api.requestPromise(settingsEndpoint, {
  148. method: 'PUT',
  149. data: {
  150. alertsMemberWrite: true,
  151. },
  152. });
  153. addSuccessMessage(t('Successfully updated organization settings'));
  154. } catch (err) {
  155. addErrorMessage(t('Unable to update organization settings'));
  156. }
  157. }
  158. const permissionTooltipText = tct(
  159. 'Ask your organization owner or manager to [settingsLink:enable alerts access] for you.',
  160. {settingsLink: <Link to={`/settings/${organization.slug}`} />}
  161. );
  162. const renderButton = (hasAccess: boolean) => (
  163. <Button
  164. disabled={!hasAccess}
  165. title={!hasAccess ? permissionTooltipText : undefined}
  166. icon={!hideIcon && <IconSiren {...iconProps} />}
  167. to={projectSlug ? createAlertUrl(projectSlug) : undefined}
  168. tooltipProps={{
  169. isHoverable: true,
  170. position: 'top',
  171. overlayStyle: {
  172. maxWidth: '270px',
  173. },
  174. }}
  175. onClick={projectSlug ? onEnter : handleClickWithoutProject}
  176. {...buttonProps}
  177. >
  178. {buttonProps.children ?? t('Create Alert')}
  179. </Button>
  180. );
  181. const showGuide = !organization.alertsMemberWrite && !!showPermissionGuide;
  182. return (
  183. <Access organization={organization} access={['alerts:write']}>
  184. {({hasAccess}) =>
  185. showGuide ? (
  186. <Access organization={organization} access={['org:write']}>
  187. {({hasAccess: isOrgAdmin}) => (
  188. <GuideAnchor
  189. target={isOrgAdmin ? 'alerts_write_owner' : 'alerts_write_member'}
  190. onFinish={isOrgAdmin ? enableAlertsMemberWrite : undefined}
  191. >
  192. {renderButton(hasAccess)}
  193. </GuideAnchor>
  194. )}
  195. </Access>
  196. ) : (
  197. renderButton(hasAccess)
  198. )
  199. }
  200. </Access>
  201. );
  202. }
  203. );
  204. export {CreateAlertFromViewButton};
  205. export default CreateAlertButton;