solutionsSection.tsx 7.4 KB

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