actions.tsx 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. // eslint-disable-next-line no-restricted-imports
  2. import {browserHistory, withRouter, WithRouterProps} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import {openModal} from 'sentry/actionCreators/modal';
  5. import {pinSearch, unpinSearch} from 'sentry/actionCreators/savedSearches';
  6. import Access from 'sentry/components/acl/access';
  7. import Button from 'sentry/components/button';
  8. import MenuItem from 'sentry/components/menuItem';
  9. import CreateSavedSearchModal from 'sentry/components/modals/createSavedSearchModal';
  10. import {IconAdd, IconPin, IconSliders} from 'sentry/icons';
  11. import {t} from 'sentry/locale';
  12. import {SavedSearch, SavedSearchType} from 'sentry/types';
  13. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  14. import SmartSearchBar from './index';
  15. import {removeSpace} from './utils';
  16. type SmartSearchBarProps = React.ComponentProps<typeof SmartSearchBar>;
  17. type ActionItem = NonNullable<SmartSearchBarProps['actionBarItems']>[number];
  18. type ActionProps = React.ComponentProps<ActionItem['Action']>;
  19. type PinSearchActionOpts = {
  20. /**
  21. * The current issue sort
  22. */
  23. sort: string;
  24. /**
  25. * The currently pinned search
  26. */
  27. pinnedSearch?: SavedSearch;
  28. };
  29. /**
  30. * The Pin Search action toggles the current as a pinned search
  31. */
  32. export function makePinSearchAction({pinnedSearch, sort}: PinSearchActionOpts) {
  33. const PinSearchAction = ({
  34. menuItemVariant,
  35. savedSearchType,
  36. organization,
  37. api,
  38. query,
  39. location,
  40. }: ActionProps & WithRouterProps) => {
  41. const onTogglePinnedSearch = async (evt: React.MouseEvent) => {
  42. evt.preventDefault();
  43. evt.stopPropagation();
  44. if (savedSearchType === undefined) {
  45. return;
  46. }
  47. const {cursor: _cursor, page: _page, ...currentQuery} = location.query;
  48. trackAdvancedAnalyticsEvent('search.pin', {
  49. organization,
  50. action: !!pinnedSearch ? 'unpin' : 'pin',
  51. search_type: savedSearchType === SavedSearchType.ISSUE ? 'issues' : 'events',
  52. query: pinnedSearch?.query ?? query,
  53. });
  54. if (!!pinnedSearch) {
  55. unpinSearch(api, organization.slug, savedSearchType, pinnedSearch).then(() => {
  56. browserHistory.push({
  57. ...location,
  58. pathname: `/organizations/${organization.slug}/issues/`,
  59. query: {
  60. ...currentQuery,
  61. query: pinnedSearch.query,
  62. sort: pinnedSearch.sort,
  63. },
  64. });
  65. });
  66. return;
  67. }
  68. const resp = await pinSearch(
  69. api,
  70. organization.slug,
  71. savedSearchType,
  72. removeSpace(query),
  73. sort
  74. );
  75. if (!resp || !resp.id) {
  76. return;
  77. }
  78. browserHistory.push({
  79. ...location,
  80. pathname: `/organizations/${organization.slug}/issues/searches/${resp.id}/`,
  81. query: currentQuery,
  82. });
  83. };
  84. const pinTooltip = !!pinnedSearch ? t('Unpin this search') : t('Pin this search');
  85. return menuItemVariant ? (
  86. <MenuItem
  87. withBorder
  88. data-test-id="pin-icon"
  89. icon={<IconPin isSolid={!!pinnedSearch} size="xs" />}
  90. onClick={onTogglePinnedSearch}
  91. >
  92. {!!pinnedSearch ? t('Unpin Search') : t('Pin Search')}
  93. </MenuItem>
  94. ) : (
  95. <ActionButton
  96. title={pinTooltip}
  97. disabled={!query}
  98. aria-label={pinTooltip}
  99. onClick={onTogglePinnedSearch}
  100. isActive={!!pinnedSearch}
  101. data-test-id="pin-icon"
  102. icon={<IconPin isSolid={!!pinnedSearch} size="xs" />}
  103. />
  104. );
  105. };
  106. return {key: 'pinSearch', Action: withRouter(PinSearchAction)};
  107. }
  108. type SaveSearchActionOpts = {
  109. /**
  110. * The current issue sort
  111. */
  112. sort: string;
  113. };
  114. /**
  115. * The Save Search action triggers the create saved search modal from the
  116. * current query.
  117. */
  118. export function makeSaveSearchAction({sort}: SaveSearchActionOpts) {
  119. const SavedSearchAction = ({menuItemVariant, query, organization}: ActionProps) => {
  120. const onClick = () =>
  121. openModal(deps => (
  122. <CreateSavedSearchModal {...deps} {...{organization, query, sort}} />
  123. ));
  124. return (
  125. <Access organization={organization} access={['org:write']}>
  126. {({hasAccess}) => {
  127. const title = hasAccess
  128. ? t('Add to organization saved searches')
  129. : t('You do not have permission to create a saved search');
  130. return menuItemVariant ? (
  131. <MenuItem
  132. onClick={onClick}
  133. disabled={!hasAccess}
  134. icon={<IconAdd size="xs" />}
  135. title={!hasAccess ? title : undefined}
  136. withBorder
  137. >
  138. {t('Create Saved Search')}
  139. </MenuItem>
  140. ) : (
  141. <ActionButton
  142. onClick={onClick}
  143. disabled={!hasAccess}
  144. icon={<IconAdd size="xs" />}
  145. title={title}
  146. aria-label={title}
  147. data-test-id="save-current-search"
  148. />
  149. );
  150. }}
  151. </Access>
  152. );
  153. };
  154. return {key: 'saveSearch', Action: SavedSearchAction};
  155. }
  156. type SearchBuilderActionOpts = {
  157. onSidebarToggle: React.MouseEventHandler;
  158. };
  159. /**
  160. * The Search Builder action toggles the Issue Stream search builder
  161. */
  162. export function makeSearchBuilderAction({onSidebarToggle}: SearchBuilderActionOpts) {
  163. const SearchBuilderAction = ({menuItemVariant}: ActionProps) =>
  164. menuItemVariant ? (
  165. <MenuItem withBorder icon={<IconSliders size="xs" />} onClick={onSidebarToggle}>
  166. {t('Toggle sidebar')}
  167. </MenuItem>
  168. ) : (
  169. <ActionButton
  170. title={t('Toggle search builder')}
  171. tooltipProps={{containerDisplayMode: 'inline-flex'}}
  172. aria-label={t('Toggle search builder')}
  173. onClick={onSidebarToggle}
  174. icon={<IconSliders size="xs" />}
  175. />
  176. );
  177. return {key: 'searchBuilder', Action: SearchBuilderAction};
  178. }
  179. export const ActionButton = styled(Button)<{isActive?: boolean}>`
  180. color: ${p => (p.isActive ? p.theme.blue300 : p.theme.gray300)};
  181. width: 18px;
  182. &,
  183. &:hover,
  184. &:focus {
  185. background: transparent;
  186. }
  187. &:hover {
  188. color: ${p => p.theme.gray400};
  189. }
  190. `;
  191. ActionButton.defaultProps = {
  192. type: 'button',
  193. borderless: true,
  194. size: 'zero',
  195. };