step.tsx 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. import {Fragment, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import beautify from 'js-beautify';
  4. import {Button} from 'sentry/components/button';
  5. import {OnboardingCodeSnippet} from 'sentry/components/onboarding/gettingStartedDoc/onboardingCodeSnippet';
  6. import {IconChevron} from 'sentry/icons';
  7. import {t} from 'sentry/locale';
  8. import {space} from 'sentry/styles/space';
  9. export enum StepType {
  10. INSTALL = 'install',
  11. CONFIGURE = 'configure',
  12. VERIFY = 'verify',
  13. }
  14. export const StepTitle = {
  15. [StepType.INSTALL]: t('Install'),
  16. [StepType.CONFIGURE]: t('Configure SDK'),
  17. [StepType.VERIFY]: t('Verify'),
  18. };
  19. interface CodeSnippetTab {
  20. code: string;
  21. label: string;
  22. language: string;
  23. value: string;
  24. filename?: string;
  25. }
  26. interface TabbedCodeSnippetProps {
  27. /**
  28. * An array of tabs to be displayed
  29. */
  30. tabs: CodeSnippetTab[];
  31. /**
  32. * A callback to be invoked when the configuration is copied to the clipboard
  33. */
  34. onCopy?: () => void;
  35. /**
  36. * A callback to be invoked when the configuration is selected and copied to the clipboard
  37. */
  38. onSelectAndCopy?: () => void;
  39. /**
  40. * Whether or not the configuration or parts of it are currently being loaded
  41. */
  42. partialLoading?: boolean;
  43. }
  44. export function TabbedCodeSnippet({
  45. tabs,
  46. onCopy,
  47. onSelectAndCopy,
  48. partialLoading,
  49. }: TabbedCodeSnippetProps) {
  50. const [selectedTabValue, setSelectedTabValue] = useState(tabs[0].value);
  51. const selectedTab = tabs.find(tab => tab.value === selectedTabValue) ?? tabs[0];
  52. const {code, language, filename} = selectedTab;
  53. return (
  54. <OnboardingCodeSnippet
  55. dark
  56. language={language}
  57. onCopy={onCopy}
  58. onSelectAndCopy={onSelectAndCopy}
  59. hideCopyButton={partialLoading}
  60. disableUserSelection={partialLoading}
  61. tabs={tabs}
  62. selectedTab={selectedTabValue}
  63. onTabClick={value => setSelectedTabValue(value)}
  64. filename={filename}
  65. >
  66. {language === 'javascript'
  67. ? beautify.js(code, {
  68. indent_size: 2,
  69. e4x: true,
  70. brace_style: 'preserve-inline',
  71. })
  72. : code.trim()}
  73. </OnboardingCodeSnippet>
  74. );
  75. }
  76. export type Configuration = {
  77. /**
  78. * Additional information to be displayed below the code snippet
  79. */
  80. additionalInfo?: React.ReactNode;
  81. /**
  82. * The code snippet to display
  83. */
  84. code?: string | CodeSnippetTab[];
  85. /**
  86. * Nested configurations provide a convenient way to accommodate diverse layout styles, like the Spring Boot configuration.
  87. */
  88. configurations?: Configuration[];
  89. /**
  90. * A brief description of the configuration
  91. */
  92. description?: React.ReactNode;
  93. /**
  94. * The language of the code to be rendered (python, javascript, etc)
  95. */
  96. language?: string;
  97. /**
  98. * A callback to be invoked when the configuration is copied to the clipboard
  99. */
  100. onCopy?: () => void;
  101. /**
  102. * A callback to be invoked when the configuration is selected and copied to the clipboard
  103. */
  104. onSelectAndCopy?: () => void;
  105. /**
  106. * Whether or not the configuration or parts of it are currently being loaded
  107. */
  108. partialLoading?: boolean;
  109. };
  110. // TODO(aknaus): move to types
  111. interface BaseStepProps {
  112. /**
  113. * Additional information to be displayed below the configurations
  114. */
  115. additionalInfo?: React.ReactNode;
  116. /**
  117. * Content that goes directly above the code snippet
  118. */
  119. codeHeader?: React.ReactNode;
  120. /**
  121. * Whether the step instructions are collapsible
  122. */
  123. collapsible?: boolean;
  124. /**
  125. * An array of configurations to be displayed
  126. */
  127. configurations?: Configuration[];
  128. /**
  129. * A brief description of the step
  130. */
  131. description?: React.ReactNode | React.ReactNode[];
  132. /**
  133. * Fired when the optional toggle is clicked.
  134. * Useful for when we want to fire analytics events.
  135. */
  136. onOptionalToggleClick?: (showOptionalConfig: boolean) => void;
  137. }
  138. interface StepPropsWithTitle extends BaseStepProps {
  139. title: string;
  140. type?: undefined;
  141. }
  142. interface StepPropsWithoutTitle extends BaseStepProps {
  143. type: StepType;
  144. title?: undefined;
  145. }
  146. export type StepProps = StepPropsWithTitle | StepPropsWithoutTitle;
  147. function getConfiguration({
  148. description,
  149. code,
  150. language,
  151. additionalInfo,
  152. onCopy,
  153. onSelectAndCopy,
  154. partialLoading,
  155. }: Configuration) {
  156. return (
  157. <Configuration>
  158. {description && <Description>{description}</Description>}
  159. {Array.isArray(code) ? (
  160. <TabbedCodeSnippet
  161. tabs={code}
  162. onCopy={onCopy}
  163. onSelectAndCopy={onSelectAndCopy}
  164. partialLoading={partialLoading}
  165. />
  166. ) : (
  167. language &&
  168. code && (
  169. <OnboardingCodeSnippet
  170. dark
  171. language={language}
  172. onCopy={onCopy}
  173. onSelectAndCopy={onSelectAndCopy}
  174. hideCopyButton={partialLoading}
  175. disableUserSelection={partialLoading}
  176. >
  177. {language === 'javascript'
  178. ? beautify.js(code, {
  179. indent_size: 2,
  180. e4x: true,
  181. brace_style: 'preserve-inline',
  182. })
  183. : code.trim()}
  184. </OnboardingCodeSnippet>
  185. )
  186. )}
  187. {additionalInfo && <AdditionalInfo>{additionalInfo}</AdditionalInfo>}
  188. </Configuration>
  189. );
  190. }
  191. export function Step({
  192. title,
  193. type,
  194. configurations,
  195. additionalInfo,
  196. description,
  197. onOptionalToggleClick,
  198. collapsible = false,
  199. codeHeader,
  200. }: StepProps) {
  201. const [showOptionalConfig, setShowOptionalConfig] = useState(false);
  202. const config = (
  203. <Fragment>
  204. {description && <Description>{description}</Description>}
  205. {!!configurations?.length && (
  206. <Configurations>
  207. {configurations.map((configuration, index) => {
  208. if (configuration.configurations) {
  209. return (
  210. <Fragment key={index}>
  211. {getConfiguration(configuration)}
  212. {configuration.configurations.map(
  213. (nestedConfiguration, nestedConfigurationIndex) => (
  214. <Fragment key={nestedConfigurationIndex}>
  215. {nestedConfigurationIndex ===
  216. (configuration.configurations?.length ?? 1) - 1
  217. ? codeHeader
  218. : null}
  219. {getConfiguration(nestedConfiguration)}
  220. </Fragment>
  221. )
  222. )}
  223. </Fragment>
  224. );
  225. }
  226. return (
  227. <Fragment key={index}>
  228. {index === configurations.length - 1 ? codeHeader : null}
  229. {getConfiguration(configuration)}
  230. </Fragment>
  231. );
  232. })}
  233. </Configurations>
  234. )}
  235. {additionalInfo && <GeneralAdditionalInfo>{additionalInfo}</GeneralAdditionalInfo>}
  236. </Fragment>
  237. );
  238. return collapsible ? (
  239. <div>
  240. <OptionalConfigWrapper
  241. expanded={showOptionalConfig}
  242. onClick={() => {
  243. onOptionalToggleClick?.(!showOptionalConfig);
  244. setShowOptionalConfig(!showOptionalConfig);
  245. }}
  246. >
  247. <h4 style={{marginBottom: 0}}>{title ?? StepTitle[type]}</h4>
  248. <ToggleButton
  249. priority="link"
  250. borderless
  251. size="zero"
  252. icon={<IconChevron direction={showOptionalConfig ? 'down' : 'right'} />}
  253. aria-label={t('Toggle optional configuration')}
  254. />
  255. </OptionalConfigWrapper>
  256. {showOptionalConfig ? config : null}
  257. </div>
  258. ) : (
  259. <div>
  260. <h4>{title ?? StepTitle[type]}</h4>
  261. {config}
  262. </div>
  263. );
  264. }
  265. const Configuration = styled('div')`
  266. display: flex;
  267. flex-direction: column;
  268. gap: 1rem;
  269. `;
  270. const Configurations = styled(Configuration)`
  271. margin-top: ${space(2)};
  272. `;
  273. const Description = styled('div')`
  274. code {
  275. color: ${p => p.theme.pink400};
  276. }
  277. && > p,
  278. && > h4,
  279. && > h5,
  280. && > h6 {
  281. margin-bottom: ${space(1)};
  282. }
  283. `;
  284. const AdditionalInfo = styled(Description)``;
  285. const GeneralAdditionalInfo = styled(Description)`
  286. margin-top: ${space(2)};
  287. `;
  288. const OptionalConfigWrapper = styled('div')<{expanded: boolean}>`
  289. display: flex;
  290. gap: ${space(1)};
  291. margin-bottom: ${p => (p.expanded ? space(2) : 0)};
  292. cursor: pointer;
  293. `;
  294. const ToggleButton = styled(Button)`
  295. padding: 0;
  296. &,
  297. :hover {
  298. color: ${p => p.theme.gray500};
  299. }
  300. `;