autofixSteps.tsx 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. import {Fragment, useEffect, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {Button} from 'sentry/components/button';
  4. import DateTime from 'sentry/components/dateTime';
  5. import {AutofixChanges} from 'sentry/components/events/autofix/autofixChanges';
  6. import {AutofixRootCause} from 'sentry/components/events/autofix/autofixRootCause';
  7. import {
  8. type AutofixData,
  9. type AutofixProgressItem,
  10. type AutofixStep,
  11. AutofixStepType,
  12. } from 'sentry/components/events/autofix/types';
  13. import LoadingIndicator from 'sentry/components/loadingIndicator';
  14. import Panel from 'sentry/components/panels/panel';
  15. import {
  16. IconCheckmark,
  17. IconChevron,
  18. IconClose,
  19. IconCode,
  20. IconFatal,
  21. IconQuestion,
  22. IconSad,
  23. } from 'sentry/icons';
  24. import {t} from 'sentry/locale';
  25. import {space} from 'sentry/styles/space';
  26. import usePrevious from 'sentry/utils/usePrevious';
  27. function StepIcon({step}: {step: AutofixStep}) {
  28. if (step.type === AutofixStepType.CHANGES) {
  29. return <IconCode size="sm" color="gray300" />;
  30. }
  31. if (step.type === AutofixStepType.ROOT_CAUSE_ANALYSIS) {
  32. if (step.causes?.length === 0) {
  33. return <IconSad size="sm" color="gray300" />;
  34. }
  35. return step.selection ? (
  36. <IconCheckmark size="sm" color="green300" isCircled />
  37. ) : (
  38. <IconQuestion size="sm" color="gray300" />
  39. );
  40. }
  41. switch (step.status) {
  42. case 'PROCESSING':
  43. return <ProcessingStatusIndicator size={14} mini hideMessage />;
  44. case 'CANCELLED':
  45. return <IconClose size="sm" isCircled color="gray300" />;
  46. case 'ERROR':
  47. return <IconFatal size="sm" color="red300" />;
  48. case 'COMPLETED':
  49. return <IconCheckmark size="sm" color="green300" isCircled />;
  50. default:
  51. return null;
  52. }
  53. }
  54. function stepShouldBeginExpanded(step: AutofixStep) {
  55. if (step.type === AutofixStepType.CHANGES) {
  56. return true;
  57. }
  58. if (step.type === AutofixStepType.ROOT_CAUSE_ANALYSIS) {
  59. return step.selection ? false : true;
  60. }
  61. return step.status !== 'COMPLETED';
  62. }
  63. interface StepProps {
  64. groupId: string;
  65. onRetry: () => void;
  66. runId: string;
  67. step: AutofixStep;
  68. isChild?: boolean;
  69. stepNumber?: number;
  70. }
  71. interface AutofixStepsProps {
  72. data: AutofixData;
  73. groupId: string;
  74. onRetry: () => void;
  75. runId: string;
  76. }
  77. function isProgressLog(
  78. item: AutofixProgressItem | AutofixStep
  79. ): item is AutofixProgressItem {
  80. return 'message' in item && 'timestamp' in item;
  81. }
  82. function Progress({
  83. progress,
  84. groupId,
  85. runId,
  86. onRetry,
  87. }: {
  88. groupId: string;
  89. onRetry: () => void;
  90. progress: AutofixProgressItem | AutofixStep;
  91. runId: string;
  92. }) {
  93. if (isProgressLog(progress)) {
  94. return (
  95. <Fragment>
  96. <DateTime date={progress.timestamp} format="HH:mm:ss:SSS" />
  97. <div>{progress.message}</div>
  98. </Fragment>
  99. );
  100. }
  101. return (
  102. <ProgressStepContainer>
  103. <Step step={progress} isChild groupId={groupId} runId={runId} onRetry={onRetry} />
  104. </ProgressStepContainer>
  105. );
  106. }
  107. export function Step({step, isChild, groupId, runId, onRetry}: StepProps) {
  108. const previousStepStatus = usePrevious(step.status);
  109. const isActive = step.status !== 'PENDING' && step.status !== 'CANCELLED';
  110. const [isExpanded, setIsExpanded] = useState(() => stepShouldBeginExpanded(step));
  111. useEffect(() => {
  112. if (
  113. previousStepStatus &&
  114. previousStepStatus !== step.status &&
  115. step.status === 'COMPLETED'
  116. ) {
  117. setIsExpanded(false);
  118. }
  119. }, [previousStepStatus, step.status]);
  120. const logs: AutofixProgressItem[] = step.progress?.filter(isProgressLog) ?? [];
  121. const activeLog = step.completedMessage ?? logs.at(-1)?.message ?? null;
  122. const hasContent = Boolean(
  123. step.completedMessage ||
  124. step.progress?.length ||
  125. step.type !== AutofixStepType.DEFAULT
  126. );
  127. const canToggle = Boolean(isActive && hasContent);
  128. return (
  129. <StepCard active={isActive}>
  130. <StepHeader
  131. canToggle={canToggle}
  132. isChild={isChild}
  133. onClick={() => {
  134. if (canToggle) {
  135. setIsExpanded(value => !value);
  136. }
  137. }}
  138. >
  139. <StepHeaderLeft>
  140. <StepIconContainer>
  141. <StepIcon step={step} />
  142. </StepIconContainer>
  143. <StepTitle>{step.title}</StepTitle>
  144. {activeLog && !isExpanded && (
  145. <StepHeaderDescription>{activeLog}</StepHeaderDescription>
  146. )}
  147. </StepHeaderLeft>
  148. <StepHeaderRight>
  149. {canToggle ? (
  150. <Button
  151. icon={<IconChevron size="xs" direction={isExpanded ? 'down' : 'right'} />}
  152. aria-label={t('Toggle step details')}
  153. aria-expanded={isExpanded}
  154. size="zero"
  155. borderless
  156. />
  157. ) : null}
  158. </StepHeaderRight>
  159. </StepHeader>
  160. {isExpanded && (
  161. <Fragment>
  162. {step.completedMessage && <StepBody>{step.completedMessage}</StepBody>}
  163. {step.progress && step.progress.length > 0 ? (
  164. <ProgressContainer>
  165. {step.progress.map((progress, i) => (
  166. <Progress
  167. progress={progress}
  168. key={i}
  169. groupId={groupId}
  170. runId={runId}
  171. onRetry={onRetry}
  172. />
  173. ))}
  174. </ProgressContainer>
  175. ) : null}
  176. {step.type === AutofixStepType.ROOT_CAUSE_ANALYSIS && (
  177. <AutofixRootCause
  178. groupId={groupId}
  179. runId={runId}
  180. causes={step.causes}
  181. rootCauseSelection={step.selection}
  182. />
  183. )}
  184. {step.type === AutofixStepType.CHANGES && (
  185. <AutofixChanges step={step} groupId={groupId} onRetry={onRetry} />
  186. )}
  187. </Fragment>
  188. )}
  189. </StepCard>
  190. );
  191. }
  192. export function AutofixSteps({data, groupId, runId, onRetry}: AutofixStepsProps) {
  193. return (
  194. <div>
  195. {data.steps?.map((step, index) => (
  196. <Step
  197. step={step}
  198. key={step.id}
  199. stepNumber={index + 1}
  200. groupId={groupId}
  201. runId={runId}
  202. onRetry={onRetry}
  203. />
  204. ))}
  205. </div>
  206. );
  207. }
  208. const StepCard = styled(Panel)<{active?: boolean}>`
  209. opacity: ${p => (p.active ? 1 : 0.6)};
  210. overflow: hidden;
  211. :last-child {
  212. margin-bottom: 0;
  213. }
  214. `;
  215. const StepHeader = styled('div')<{canToggle: boolean; isChild?: boolean}>`
  216. display: flex;
  217. justify-content: space-between;
  218. align-items: center;
  219. padding: ${space(2)};
  220. gap: ${space(1)};
  221. font-size: ${p => p.theme.fontSizeMedium};
  222. font-family: ${p => p.theme.text.family};
  223. cursor: ${p => (p.canToggle ? 'pointer' : 'default')};
  224. &:last-child {
  225. padding-bottom: ${space(2)};
  226. }
  227. `;
  228. const StepHeaderLeft = styled('div')`
  229. display: flex;
  230. align-items: center;
  231. flex: 1;
  232. overflow: hidden;
  233. `;
  234. const StepHeaderDescription = styled('div')`
  235. font-size: ${p => p.theme.fontSizeSmall};
  236. color: ${p => p.theme.subText};
  237. padding: 0 ${space(2)} 0 ${space(1)};
  238. margin-left: ${space(1)};
  239. border-left: 1px solid ${p => p.theme.border};
  240. flex-grow: 1;
  241. ${p => p.theme.overflowEllipsis};
  242. `;
  243. const StepIconContainer = styled('div')`
  244. display: flex;
  245. align-items: center;
  246. margin-right: ${space(1)};
  247. `;
  248. const StepHeaderRight = styled('div')`
  249. display: flex;
  250. align-items: center;
  251. gap: ${space(1)};
  252. `;
  253. const StepTitle = styled('div')`
  254. font-weight: ${p => p.theme.fontWeightBold};
  255. white-space: nowrap;
  256. display: flex;
  257. flex-shrink: 1;
  258. align-items: center;
  259. flex-grow: 0;
  260. span {
  261. margin-right: ${space(1)};
  262. }
  263. `;
  264. const StepBody = styled('p')`
  265. padding: 0 ${space(2)} ${space(2)} ${space(2)};
  266. margin: -${space(1)} 0 0 0;
  267. `;
  268. const ProcessingStatusIndicator = styled(LoadingIndicator)`
  269. && {
  270. margin: 0;
  271. height: 14px;
  272. width: 14px;
  273. }
  274. `;
  275. const ProgressContainer = styled('div')`
  276. background: ${p => p.theme.backgroundSecondary};
  277. border-top: 1px solid ${p => p.theme.border};
  278. padding: ${space(2)};
  279. display: grid;
  280. gap: ${space(1)} ${space(2)};
  281. grid-template-columns: auto 1fr;
  282. font-size: ${p => p.theme.fontSizeSmall};
  283. font-family: ${p => p.theme.text.familyMono};
  284. `;
  285. const ProgressStepContainer = styled('div')`
  286. grid-column: 1/-1;
  287. `;