solutionsSection.tsx 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. import {useRef, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import FeatureBadge from 'sentry/components/badge/featureBadge';
  4. import {Button} from 'sentry/components/button';
  5. import {Chevron} from 'sentry/components/chevron';
  6. import useDrawer from 'sentry/components/globalDrawer';
  7. import {GroupSummary} from 'sentry/components/group/groupSummary';
  8. import Placeholder from 'sentry/components/placeholder';
  9. import {IconMegaphone} from 'sentry/icons';
  10. import {t, tct} from 'sentry/locale';
  11. import {space} from 'sentry/styles/space';
  12. import type {Event} from 'sentry/types/event';
  13. import type {Group} from 'sentry/types/group';
  14. import type {Project} from 'sentry/types/project';
  15. import {getConfigForIssueType} from 'sentry/utils/issueTypeConfig';
  16. import {useFeedbackForm} from 'sentry/utils/useFeedbackForm';
  17. import {SectionKey} from 'sentry/views/issueDetails/streamline/context';
  18. import {FoldSection} from 'sentry/views/issueDetails/streamline/foldSection';
  19. import {useAiConfig} from 'sentry/views/issueDetails/streamline/hooks/useAiConfig';
  20. import Resources from 'sentry/views/issueDetails/streamline/sidebar/resources';
  21. import {SolutionsHubDrawer} from 'sentry/views/issueDetails/streamline/sidebar/solutionsHubDrawer';
  22. import {useHasStreamlinedUI} from 'sentry/views/issueDetails/utils';
  23. function SolutionsHubFeedbackButton({hidden}: {hidden: boolean}) {
  24. const openFeedbackForm = useFeedbackForm();
  25. if (hidden) {
  26. return null;
  27. }
  28. return (
  29. <Button
  30. aria-label={t('Give feedback on the solutions hub')}
  31. icon={<IconMegaphone />}
  32. size="xs"
  33. onClick={() =>
  34. openFeedbackForm?.({
  35. messagePlaceholder: t('How can we make Issue Summary better for you?'),
  36. tags: {
  37. ['feedback.source']: 'issue_details_ai_autofix',
  38. ['feedback.owner']: 'ml-ai',
  39. },
  40. })
  41. }
  42. />
  43. );
  44. }
  45. export default function SolutionsSection({
  46. group,
  47. project,
  48. event,
  49. }: {
  50. event: Event | undefined;
  51. group: Group;
  52. project: Project;
  53. }) {
  54. const hasStreamlinedUI = useHasStreamlinedUI();
  55. // We don't use this on the streamlined UI, since the section folds.
  56. const [isExpanded, setIsExpanded] = useState(false);
  57. const openButtonRef = useRef<HTMLButtonElement>(null);
  58. const {openDrawer} = useDrawer();
  59. const openSolutionsDrawer = () => {
  60. if (!event) {
  61. return;
  62. }
  63. openDrawer(
  64. () => <SolutionsHubDrawer group={group} project={project} event={event} />,
  65. {
  66. ariaLabel: t('Solutions drawer'),
  67. // We prevent a click on the Open/Close Autofix button from closing the drawer so that
  68. // we don't reopen it immediately, and instead let the button handle this itself.
  69. shouldCloseOnInteractOutside: element => {
  70. const viewAllButton = openButtonRef.current;
  71. if (
  72. viewAllButton?.contains(element) ||
  73. document.getElementById('sentry-feedback')?.contains(element) ||
  74. document.getElementById('autofix-rethink-input')?.contains(element) ||
  75. document.getElementById('autofix-output-stream')?.contains(element) ||
  76. document.getElementById('autofix-write-access-modal')?.contains(element) ||
  77. element.closest('[data-overlay="true"]')
  78. ) {
  79. return false;
  80. }
  81. return true;
  82. },
  83. transitionProps: {stiffness: 1000},
  84. }
  85. );
  86. };
  87. const aiConfig = useAiConfig(group, event, project);
  88. const issueTypeConfig = getConfigForIssueType(group, project);
  89. const showCtaButton =
  90. aiConfig.needsGenAIConsent ||
  91. aiConfig.hasAutofix ||
  92. (aiConfig.hasSummary && aiConfig.hasResources);
  93. const isButtonLoading = aiConfig.isAutofixSetupLoading;
  94. const getButtonText = () => {
  95. if (aiConfig.needsGenAIConsent) {
  96. return t('Set up Sentry AI');
  97. }
  98. if (!aiConfig.hasAutofix) {
  99. return t('Open Resources');
  100. }
  101. if (aiConfig.needsAutofixSetup) {
  102. return t('Set up Autofix');
  103. }
  104. return aiConfig.hasResources ? t('Open Resources & Autofix') : t('Open Autofix');
  105. };
  106. const renderContent = () => {
  107. if (aiConfig.needsGenAIConsent) {
  108. return (
  109. <Summary>
  110. {t('Explore potential root causes and solutions with Sentry AI.')}
  111. </Summary>
  112. );
  113. }
  114. if (aiConfig.hasSummary) {
  115. return (
  116. <Summary>
  117. <GroupSummary group={group} event={event} project={project} preview />
  118. </Summary>
  119. );
  120. }
  121. if (!aiConfig.hasSummary && issueTypeConfig.resources) {
  122. return (
  123. <ResourcesWrapper isExpanded={hasStreamlinedUI ? true : isExpanded}>
  124. <ResourcesContent isExpanded={hasStreamlinedUI ? true : isExpanded}>
  125. <Resources
  126. configResources={issueTypeConfig.resources}
  127. eventPlatform={event?.platform}
  128. group={group}
  129. />
  130. </ResourcesContent>
  131. {!hasStreamlinedUI && (
  132. <ExpandButton onClick={() => setIsExpanded(!isExpanded)} size="zero">
  133. {isExpanded ? t('SHOW LESS') : t('READ MORE')}
  134. </ExpandButton>
  135. )}
  136. </ResourcesWrapper>
  137. );
  138. }
  139. return null;
  140. };
  141. const titleComponent = (
  142. <HeaderContainer>
  143. {t('Solutions Hub')}
  144. {aiConfig.hasSummary && (
  145. <FeatureBadge
  146. type="beta"
  147. title={tct(
  148. 'This feature is in beta. Try it out and let us know your feedback at [email:autofix@sentry.io].',
  149. {
  150. email: <a href="mailto:autofix@sentry.io" />,
  151. }
  152. )}
  153. />
  154. )}
  155. </HeaderContainer>
  156. );
  157. return (
  158. <SidebarFoldSection
  159. title={titleComponent}
  160. sectionKey={SectionKey.SOLUTIONS_HUB}
  161. actions={<SolutionsHubFeedbackButton hidden={!aiConfig.hasSummary} />}
  162. preventCollapse={!hasStreamlinedUI}
  163. >
  164. <SolutionsSectionContainer>
  165. {renderContent()}
  166. {isButtonLoading ? (
  167. <ButtonPlaceholder />
  168. ) : showCtaButton ? (
  169. <StyledButton
  170. ref={openButtonRef}
  171. onClick={() => openSolutionsDrawer()}
  172. analyticsEventKey="issue_details.solutions_hub_opened"
  173. analyticsEventName="Issue Details: Solutions Hub Opened"
  174. analyticsParams={{
  175. has_streamlined_ui: hasStreamlinedUI,
  176. }}
  177. >
  178. {getButtonText()}
  179. <ChevronContainer>
  180. <Chevron direction="right" size="large" />
  181. </ChevronContainer>
  182. </StyledButton>
  183. ) : null}
  184. </SolutionsSectionContainer>
  185. </SidebarFoldSection>
  186. );
  187. }
  188. const SidebarFoldSection = styled(FoldSection)`
  189. font-size: ${p => p.theme.fontSizeMedium};
  190. margin: -${space(1)};
  191. `;
  192. const SolutionsSectionContainer = styled('div')`
  193. display: flex;
  194. flex-direction: column;
  195. `;
  196. const Summary = styled('div')`
  197. margin-bottom: ${space(0.5)};
  198. position: relative;
  199. `;
  200. const ResourcesWrapper = styled('div')<{isExpanded: boolean}>`
  201. position: relative;
  202. margin-bottom: ${space(1)};
  203. `;
  204. const ResourcesContent = styled('div')<{isExpanded: boolean}>`
  205. position: relative;
  206. max-height: ${p => (p.isExpanded ? 'none' : '68px')};
  207. overflow: hidden;
  208. padding-bottom: ${p => (p.isExpanded ? space(2) : 0)};
  209. ${p =>
  210. !p.isExpanded &&
  211. `
  212. &::after {
  213. content: '';
  214. position: absolute;
  215. bottom: 0;
  216. left: 0;
  217. right: 0;
  218. height: 40px;
  219. background: linear-gradient(transparent, ${p.theme.background});
  220. }
  221. `}
  222. `;
  223. const ExpandButton = styled(Button)`
  224. position: absolute;
  225. bottom: -${space(1)};
  226. right: 0;
  227. font-size: ${p => p.theme.fontSizeExtraSmall};
  228. color: ${p => p.theme.gray300};
  229. border: none;
  230. box-shadow: none;
  231. &:hover {
  232. color: ${p => p.theme.gray400};
  233. }
  234. `;
  235. const StyledButton = styled(Button)`
  236. margin-top: ${space(1)};
  237. width: 100%;
  238. background: ${p => p.theme.background}
  239. linear-gradient(to right, ${p => p.theme.background}, ${p => p.theme.pink400}20);
  240. color: ${p => p.theme.pink400};
  241. `;
  242. const ChevronContainer = styled('div')`
  243. margin-left: ${space(0.5)};
  244. height: 16px;
  245. width: 16px;
  246. `;
  247. const HeaderContainer = styled('div')`
  248. font-size: ${p => p.theme.fontSizeMedium};
  249. display: flex;
  250. align-items: center;
  251. gap: ${space(0.25)};
  252. `;
  253. const ButtonPlaceholder = styled(Placeholder)`
  254. width: 100%;
  255. height: 38px;
  256. border-radius: ${p => p.theme.borderRadius};
  257. margin-top: ${space(1)};
  258. `;