createAlertButton.tsx 6.0 KB

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