solutionsSection.tsx 7.6 KB

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