addViewPage.tsx 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336
  1. import {useContext} from 'react';
  2. import styled from '@emotion/styled';
  3. import bannerStar from 'sentry-images/spot/banner-star.svg';
  4. import {Button} from 'sentry/components/button';
  5. import InteractionStateLayer from 'sentry/components/interactionStateLayer';
  6. import QuestionTooltip from 'sentry/components/questionTooltip';
  7. import {FormattedQuery} from 'sentry/components/searchQueryBuilder/formattedQuery';
  8. import {IconMegaphone} from 'sentry/icons';
  9. import {t} from 'sentry/locale';
  10. import {space} from 'sentry/styles/space';
  11. import type {SavedSearch} from 'sentry/types/group';
  12. import {trackAnalytics} from 'sentry/utils/analytics';
  13. import {useFeedbackForm} from 'sentry/utils/useFeedbackForm';
  14. import useOrganization from 'sentry/utils/useOrganization';
  15. import {OverflowEllipsisTextContainer} from 'sentry/views/insights/common/components/textAlign';
  16. import {NewTabContext} from 'sentry/views/issueList/utils/newTabContext';
  17. type SearchSuggestion = {
  18. label: string;
  19. query: string;
  20. };
  21. interface SearchSuggestionListProps {
  22. searchSuggestions: SearchSuggestion[];
  23. title: React.ReactNode;
  24. type: 'recommended' | 'saved_searches';
  25. }
  26. const RECOMMENDED_SEARCHES: SearchSuggestion[] = [
  27. {label: 'Prioritized', query: 'is:unresolved issue.priority:[high, medium]'},
  28. {label: 'Assigned to Me', query: 'is:unresolved assigned_or_suggested:me'},
  29. {
  30. label: 'For Review',
  31. query: 'is:unresolved is:for_review assigned_or_suggested:[me, my_teams, none]',
  32. },
  33. {label: 'Request Errors', query: 'is:unresolved http.status_code:5*'},
  34. {label: 'High Volume Issues', query: 'is:unresolved timesSeen:>100'},
  35. {label: 'Recent Errors', query: 'is:unresolved issue.category:error firstSeen:-24h'},
  36. {label: 'Function Regressions', query: 'issue.type:profile_function_regression'},
  37. ];
  38. function AddViewPage({savedSearches}: {savedSearches: SavedSearch[]}) {
  39. const savedSearchTitle = (
  40. <SavedSearchesTitle>
  41. {t('Saved Searches (will be deprecated)')}
  42. <QuestionTooltip
  43. icon="info"
  44. title={t(
  45. 'Saved searches will be deprecated soon. For any you wish to return to, please save them as views.'
  46. )}
  47. size="sm"
  48. position="top"
  49. skipWrapper
  50. />
  51. </SavedSearchesTitle>
  52. );
  53. return (
  54. <AddViewWrapper>
  55. <Banner>
  56. <BannerStar1 src={bannerStar} />
  57. <BannerStar2 src={bannerStar} />
  58. <BannerStar3 src={bannerStar} />
  59. <Title>{t('Find what you need, faster')}</Title>
  60. <SubTitle>
  61. {t(
  62. "Save your issue searches for quick access. Views are for your eyes only – no need to worry about messing up other team members' views."
  63. )}
  64. </SubTitle>
  65. <FeedbackButton />
  66. </Banner>
  67. <SearchSuggestionList
  68. title={'Recommended Searches'}
  69. searchSuggestions={RECOMMENDED_SEARCHES}
  70. type="recommended"
  71. />
  72. {savedSearches && savedSearches.length !== 0 && (
  73. <SearchSuggestionList
  74. title={savedSearchTitle}
  75. searchSuggestions={savedSearches.map(search => {
  76. return {
  77. label: search.name,
  78. query: search.query,
  79. };
  80. })}
  81. type="saved_searches"
  82. />
  83. )}
  84. </AddViewWrapper>
  85. );
  86. }
  87. function SearchSuggestionList({
  88. title,
  89. searchSuggestions,
  90. type,
  91. }: SearchSuggestionListProps) {
  92. const {onNewViewSaved} = useContext(NewTabContext);
  93. const organization = useOrganization();
  94. return (
  95. <Suggestions>
  96. <TitleWrapper>{title}</TitleWrapper>
  97. <SuggestionList>
  98. {searchSuggestions.map((suggestion, index) => (
  99. <Suggestion
  100. key={index}
  101. onClick={() => {
  102. onNewViewSaved?.(suggestion.label, suggestion.query, false);
  103. const analyticsKey =
  104. type === 'recommended'
  105. ? 'issue_views.add_view.recommended_view_saved'
  106. : 'issue_views.add_view.saved_search_saved';
  107. trackAnalytics(analyticsKey, {
  108. organization,
  109. persisted: false,
  110. label: suggestion.label,
  111. query: suggestion.query,
  112. });
  113. }}
  114. >
  115. {/*
  116. Saved search labels have an average length of approximately 16 characters
  117. This container fits 16 'a's comfortably, and 20 'a's before overflowing.
  118. */}
  119. <StyledOverflowEllipsisTextContainer>
  120. {suggestion.label}
  121. </StyledOverflowEllipsisTextContainer>
  122. <QueryWrapper>
  123. <FormattedQuery query={suggestion.query} />
  124. <ActionsWrapper className="data-actions-wrapper">
  125. <StyledButton
  126. size="zero"
  127. onClick={e => {
  128. e.stopPropagation();
  129. onNewViewSaved?.(suggestion.label, suggestion.query, true);
  130. const analyticsKey =
  131. type === 'recommended'
  132. ? 'issue_views.add_view.recommended_view_saved'
  133. : 'issue_views.add_view.saved_search_saved';
  134. trackAnalytics(analyticsKey, {
  135. organization,
  136. persisted: true,
  137. label: suggestion.label,
  138. query: suggestion.query,
  139. });
  140. }}
  141. borderless
  142. >
  143. {t('Save as new view')}
  144. </StyledButton>
  145. </ActionsWrapper>
  146. </QueryWrapper>
  147. <StyledInteractionStateLayer />
  148. </Suggestion>
  149. ))}
  150. </SuggestionList>
  151. </Suggestions>
  152. );
  153. }
  154. function FeedbackButton() {
  155. const openForm = useFeedbackForm();
  156. if (!openForm) {
  157. return null;
  158. }
  159. return (
  160. <Button
  161. size="xs"
  162. icon={<IconMegaphone />}
  163. onClick={() =>
  164. openForm({
  165. messagePlaceholder: t('How can we make custom views better for you?'),
  166. tags: {
  167. ['feedback.source']: 'custom_views',
  168. ['feedback.owner']: 'issues',
  169. },
  170. })
  171. }
  172. style={{width: 'fit-content'}}
  173. >
  174. {t('Give Feedback')}
  175. </Button>
  176. );
  177. }
  178. export default AddViewPage;
  179. const Suggestions = styled('section')`
  180. width: 100%;
  181. `;
  182. const SavedSearchesTitle = styled('div')`
  183. align-items: center;
  184. display: flex;
  185. gap: ${space(1)};
  186. `;
  187. const StyledInteractionStateLayer = styled(InteractionStateLayer)`
  188. border-radius: 4px;
  189. width: 100.8%;
  190. `;
  191. const StyledOverflowEllipsisTextContainer = styled(OverflowEllipsisTextContainer)`
  192. width: 170px;
  193. `;
  194. const TitleWrapper = styled('div')`
  195. color: ${p => p.theme.subText};
  196. font-weight: 550;
  197. font-size: ${p => p.theme.fontSizeMedium};
  198. margin-bottom: ${space(0.75)};
  199. `;
  200. const ActionsWrapper = styled('div')`
  201. display: flex;
  202. align-items: center;
  203. gap: ${space(0.75)};
  204. visibility: hidden;
  205. `;
  206. const StyledButton = styled(Button)`
  207. font-size: ${p => p.theme.fontSizeMedium};
  208. color: ${p => p.theme.subText};
  209. font-weight: ${p => p.theme.fontWeightNormal};
  210. padding: ${space(0.5)};
  211. border: none;
  212. &:hover {
  213. color: ${p => p.theme.subText};
  214. }
  215. `;
  216. const QueryWrapper = styled('div')`
  217. display: flex;
  218. justify-content: space-between;
  219. align-items: center;
  220. width: 100%;
  221. overflow: hidden;
  222. `;
  223. const Suggestion = styled('li')`
  224. position: relative;
  225. display: inline-grid;
  226. grid-template-columns: 170px auto;
  227. align-items: center;
  228. padding: ${space(1)} 0;
  229. border-bottom: 1px solid ${p => p.theme.innerBorder};
  230. gap: ${space(1)};
  231. &:hover {
  232. cursor: pointer;
  233. }
  234. &:hover .data-actions-wrapper {
  235. visibility: visible;
  236. }
  237. `;
  238. const SuggestionList = styled('ul')`
  239. display: flex;
  240. flex-direction: column;
  241. padding: 0;
  242. `;
  243. const Banner = styled('div')`
  244. position: relative;
  245. display: flex;
  246. flex-direction: column;
  247. margin-bottom: 0;
  248. padding: 12px;
  249. gap: ${space(0.5)};
  250. border: 1px solid ${p => p.theme.border};
  251. border-radius: ${p => p.theme.panelBorderRadius};
  252. background: linear-gradient(
  253. 269.35deg,
  254. ${p => p.theme.backgroundTertiary} 0.32%,
  255. rgba(245, 243, 247, 0) 99.69%
  256. );
  257. `;
  258. const Title = styled('div')`
  259. font-size: ${p => p.theme.fontSizeMedium};
  260. font-weight: ${p => p.theme.fontWeightBold};
  261. `;
  262. const SubTitle = styled('div')`
  263. font-weight: ${p => p.theme.fontWeightNormal};
  264. font-size: ${p => p.theme.fontSizeMedium};
  265. `;
  266. const AddViewWrapper = styled('div')`
  267. display: flex;
  268. flex-direction: column;
  269. gap: ${space(2)};
  270. @media (max-width: ${p => p.theme.breakpoints.small}) {
  271. flex-direction: column;
  272. align-items: center;
  273. }
  274. `;
  275. const BannerStar1 = styled('img')`
  276. position: absolute;
  277. bottom: 10px;
  278. right: 150px;
  279. transform: scale(0.9);
  280. @media (max-width: ${p => p.theme.breakpoints.large}) {
  281. display: none;
  282. }
  283. `;
  284. const BannerStar2 = styled('img')`
  285. position: absolute;
  286. top: 10px;
  287. right: 120px;
  288. transform: rotate(-30deg) scale(0.7);
  289. @media (max-width: ${p => p.theme.breakpoints.large}) {
  290. display: none;
  291. }
  292. `;
  293. const BannerStar3 = styled('img')`
  294. position: absolute;
  295. bottom: 30px;
  296. right: 80px;
  297. transform: rotate(80deg) scale(0.6);
  298. @media (max-width: ${p => p.theme.breakpoints.large}) {
  299. display: none;
  300. }
  301. `;