createAlertButton.tsx 6.0 KB

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