solutionsSection.tsx 7.5 KB

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