autofixSteps.tsx 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  1. import {Fragment, 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 type {
  6. AutofixData,
  7. AutofixProgressItem,
  8. AutofixStep,
  9. } from 'sentry/components/events/autofix/types';
  10. import LoadingIndicator from 'sentry/components/loadingIndicator';
  11. import Panel from 'sentry/components/panels/panel';
  12. import {IconCheckmark, IconChevron, IconClose, IconFatal} from 'sentry/icons';
  13. import {t} from 'sentry/locale';
  14. import {space} from 'sentry/styles/space';
  15. interface StepIconProps {
  16. status: AutofixStep['status'];
  17. }
  18. function StepIcon({status}: StepIconProps) {
  19. switch (status) {
  20. case 'PROCESSING':
  21. return <ProcessingStatusIndicator size={14} mini hideMessage />;
  22. case 'CANCELLED':
  23. return <IconClose size="sm" isCircled color="gray300" />;
  24. case 'ERROR':
  25. return <IconFatal size="sm" color="red300" />;
  26. case 'COMPLETED':
  27. return <IconCheckmark size="sm" color="green300" isCircled />;
  28. default:
  29. return null;
  30. }
  31. }
  32. interface StepProps {
  33. step: AutofixStep;
  34. isChild?: boolean;
  35. stepNumber?: number;
  36. }
  37. interface AutofixStepsProps {
  38. data: AutofixData;
  39. }
  40. function isProgressLog(
  41. item: AutofixProgressItem | AutofixStep
  42. ): item is AutofixProgressItem {
  43. return 'message' in item && 'timestamp' in item;
  44. }
  45. function Progress({progress}: {progress: AutofixProgressItem | AutofixStep}) {
  46. if (isProgressLog(progress)) {
  47. return (
  48. <Fragment>
  49. <DateTime date={progress.timestamp} format="HH:mm:ss:SSS" />
  50. <div>{progress.message}</div>
  51. </Fragment>
  52. );
  53. }
  54. return (
  55. <ProgressStepContainer>
  56. <Step step={progress} isChild />
  57. </ProgressStepContainer>
  58. );
  59. }
  60. export function Step({step, isChild}: StepProps) {
  61. const isActive = step.status !== 'PENDING' && step.status !== 'CANCELLED';
  62. const [isExpanded, setIsExpanded] = useState(false);
  63. const logs = step.progress?.filter(isProgressLog) ?? [];
  64. const activeLog = step.completedMessage ?? logs.at(-1)?.message ?? null;
  65. const hasContent = step.completedMessage || step.progress?.length;
  66. return (
  67. <StepCard active={isActive}>
  68. <StepHeader
  69. isActive={isActive}
  70. isChild={isChild}
  71. onClick={() => {
  72. if (isActive && hasContent) {
  73. setIsExpanded(value => !value);
  74. }
  75. }}
  76. >
  77. <StepHeaderLeft>
  78. <StepIconContainer>
  79. <StepIcon status={step.status} />
  80. </StepIconContainer>
  81. <StepTitle>{step.title}</StepTitle>
  82. {activeLog && !isExpanded && (
  83. <StepHeaderDescription>{activeLog}</StepHeaderDescription>
  84. )}
  85. </StepHeaderLeft>
  86. <StepHeaderRight>
  87. {isActive && hasContent ? (
  88. <Button
  89. icon={<IconChevron size="xs" direction={isExpanded ? 'down' : 'right'} />}
  90. aria-label={t('Toggle step details')}
  91. aria-expanded={isExpanded}
  92. size="zero"
  93. borderless
  94. />
  95. ) : null}
  96. </StepHeaderRight>
  97. </StepHeader>
  98. {isExpanded && (
  99. <Fragment>
  100. {step.completedMessage && <StepBody>{step.completedMessage}</StepBody>}
  101. {step.progress && step.progress.length > 0 ? (
  102. <ProgressContainer>
  103. {step.progress.map((progress, i) => (
  104. <Progress progress={progress} key={i} />
  105. ))}
  106. </ProgressContainer>
  107. ) : null}
  108. </Fragment>
  109. )}
  110. </StepCard>
  111. );
  112. }
  113. export function AutofixSteps({data}: AutofixStepsProps) {
  114. return (
  115. <div>
  116. {data.steps?.map((step, index) => (
  117. <Step step={step} key={step.id} stepNumber={index + 1} />
  118. ))}
  119. </div>
  120. );
  121. }
  122. const StepCard = styled(Panel)<{active?: boolean}>`
  123. opacity: ${p => (p.active ? 1 : 0.6)};
  124. overflow: hidden;
  125. :last-child {
  126. margin-bottom: 0;
  127. }
  128. `;
  129. const StepHeader = styled('div')<{isActive: boolean; isChild?: boolean}>`
  130. display: flex;
  131. justify-content: space-between;
  132. align-items: center;
  133. padding: ${space(2)};
  134. font-size: ${p => p.theme.fontSizeMedium};
  135. font-family: ${p => p.theme.text.family};
  136. cursor: ${p => (p.isActive ? 'pointer' : 'default')};
  137. &:last-child {
  138. padding-bottom: ${space(2)};
  139. }
  140. `;
  141. const StepHeaderLeft = styled('div')`
  142. display: flex;
  143. align-items: center;
  144. flex: 1;
  145. overflow: hidden;
  146. `;
  147. const StepHeaderDescription = styled('div')`
  148. font-size: ${p => p.theme.fontSizeSmall};
  149. color: ${p => p.theme.subText};
  150. padding: 0 ${space(2)} 0 ${space(1)};
  151. margin-left: ${space(1)};
  152. border-left: 1px solid ${p => p.theme.border};
  153. flex-grow: 1;
  154. ${p => p.theme.overflowEllipsis};
  155. `;
  156. const StepIconContainer = styled('div')`
  157. display: flex;
  158. align-items: center;
  159. margin-right: ${space(1)};
  160. `;
  161. const StepHeaderRight = styled('div')`
  162. display: flex;
  163. align-items: center;
  164. gap: ${space(1)};
  165. `;
  166. const StepTitle = styled('div')`
  167. font-weight: bold;
  168. white-space: nowrap;
  169. display: flex;
  170. flex-shrink: 1;
  171. align-items: center;
  172. flex-grow: 0;
  173. span {
  174. margin-right: ${space(1)};
  175. }
  176. `;
  177. const StepBody = styled('p')`
  178. padding: 0 ${space(2)} ${space(2)} ${space(2)};
  179. margin: -${space(1)} 0 0 0;
  180. `;
  181. const ProcessingStatusIndicator = styled(LoadingIndicator)`
  182. && {
  183. margin: 0;
  184. height: 14px;
  185. width: 14px;
  186. }
  187. `;
  188. const ProgressContainer = styled('div')`
  189. background: ${p => p.theme.backgroundSecondary};
  190. border-top: 1px solid ${p => p.theme.border};
  191. padding: ${space(2)};
  192. display: grid;
  193. gap: ${space(1)} ${space(2)};
  194. grid-template-columns: auto 1fr;
  195. font-size: ${p => p.theme.fontSizeSmall};
  196. font-family: ${p => p.theme.text.familyMono};
  197. `;
  198. const ProgressStepContainer = styled('div')`
  199. grid-column: 1/-1;
  200. `;