createAlertButton.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216
  1. import {
  2. addErrorMessage,
  3. addLoadingMessage,
  4. addSuccessMessage,
  5. } from 'sentry/actionCreators/indicator';
  6. import {navigateTo} from 'sentry/actionCreators/navigation';
  7. import {hasEveryAccess} from 'sentry/components/acl/access';
  8. import GuideAnchor from 'sentry/components/assistant/guideAnchor';
  9. import {Button, ButtonProps} from 'sentry/components/button';
  10. import Link from 'sentry/components/links/link';
  11. import {IconSiren} from 'sentry/icons';
  12. import type {SVGIconProps} from 'sentry/icons/svgIcon';
  13. import {t, tct} from 'sentry/locale';
  14. import type {Organization, Project} from 'sentry/types';
  15. import type EventView from 'sentry/utils/discover/eventView';
  16. import useApi from 'sentry/utils/useApi';
  17. import useRouter from 'sentry/utils/useRouter';
  18. import withProjects from 'sentry/utils/withProjects';
  19. import {
  20. AlertType,
  21. AlertWizardAlertNames,
  22. AlertWizardRuleTemplates,
  23. DEFAULT_WIZARD_TEMPLATE,
  24. } from 'sentry/views/alerts/wizard/options';
  25. export type CreateAlertFromViewButtonProps = Omit<ButtonProps, 'aria-label'> & {
  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. projects={projects}
  89. onClick={handleClick}
  90. to={to}
  91. aria-label={t('Create Alert')}
  92. {...buttonProps}
  93. />
  94. );
  95. }
  96. type CreateAlertButtonProps = {
  97. organization: Organization;
  98. projects: Project[];
  99. alertOption?: keyof typeof AlertWizardAlertNames;
  100. hideIcon?: boolean;
  101. iconProps?: SVGIconProps;
  102. /**
  103. * Callback when the button is clicked.
  104. * This is different from `onClick` which always overrides the default
  105. * behavior when the button was clicked.
  106. */
  107. onEnter?: () => void;
  108. projectSlug?: string;
  109. referrer?: string;
  110. showPermissionGuide?: boolean;
  111. } & ButtonProps;
  112. function CreateAlertButton({
  113. organization,
  114. projects,
  115. projectSlug,
  116. iconProps,
  117. referrer,
  118. hideIcon,
  119. showPermissionGuide,
  120. alertOption,
  121. onEnter,
  122. ...buttonProps
  123. }: CreateAlertButtonProps) {
  124. const router = useRouter();
  125. const api = useApi();
  126. const createAlertUrl = (providedProj: string): string => {
  127. const params = new URLSearchParams();
  128. if (referrer) {
  129. params.append('referrer', referrer);
  130. }
  131. if (providedProj !== ':projectId') {
  132. params.append('project', providedProj);
  133. }
  134. if (alertOption) {
  135. params.append('alert_option', alertOption);
  136. }
  137. return `/organizations/${organization.slug}/alerts/wizard/?${params.toString()}`;
  138. };
  139. function handleClickWithoutProject(event: React.MouseEvent) {
  140. event.preventDefault();
  141. onEnter?.();
  142. navigateTo(createAlertUrl(':projectId'), router);
  143. }
  144. async function enableAlertsMemberWrite() {
  145. const settingsEndpoint = `/organizations/${organization.slug}/`;
  146. addLoadingMessage();
  147. try {
  148. await api.requestPromise(settingsEndpoint, {
  149. method: 'PUT',
  150. data: {
  151. alertsMemberWrite: true,
  152. },
  153. });
  154. addSuccessMessage(t('Successfully updated organization settings'));
  155. } catch (err) {
  156. addErrorMessage(t('Unable to update organization settings'));
  157. }
  158. }
  159. const permissionTooltipText = tct(
  160. 'Ask your organization owner or manager to [settingsLink:enable alerts access] for you.',
  161. {settingsLink: <Link to={`/settings/${organization.slug}`} />}
  162. );
  163. const renderButton = (hasAccess: boolean) => (
  164. <Button
  165. disabled={!hasAccess}
  166. title={!hasAccess ? permissionTooltipText : undefined}
  167. icon={!hideIcon && <IconSiren {...iconProps} />}
  168. to={projectSlug ? createAlertUrl(projectSlug) : undefined}
  169. tooltipProps={{
  170. isHoverable: true,
  171. position: 'top',
  172. overlayStyle: {
  173. maxWidth: '270px',
  174. },
  175. }}
  176. onClick={projectSlug ? onEnter : handleClickWithoutProject}
  177. {...buttonProps}
  178. >
  179. {buttonProps.children ?? t('Create Alert')}
  180. </Button>
  181. );
  182. const showGuide = !organization.alertsMemberWrite && !!showPermissionGuide;
  183. const canCreateAlert =
  184. hasEveryAccess(['alerts:write'], {organization}) ||
  185. projects.some(p => hasEveryAccess(['alerts:write'], {project: p}));
  186. const hasOrgWrite = hasEveryAccess(['org:write'], {organization});
  187. return showGuide ? (
  188. <GuideAnchor
  189. target={hasOrgWrite ? 'alerts_write_owner' : 'alerts_write_member'}
  190. onFinish={hasOrgWrite ? enableAlertsMemberWrite : undefined}
  191. >
  192. {renderButton(canCreateAlert)}
  193. </GuideAnchor>
  194. ) : (
  195. renderButton(canCreateAlert)
  196. );
  197. }
  198. export {CreateAlertFromViewButton};
  199. export default withProjects(CreateAlertButton);