documentationSetup.tsx 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. import 'prism-sentry/index.css';
  2. import {Component, Fragment} from 'react';
  3. import {css} from '@emotion/react';
  4. import styled from '@emotion/styled';
  5. import {motion} from 'framer-motion';
  6. import {loadDocs} from 'sentry/actionCreators/projects';
  7. import {Client} from 'sentry/api';
  8. import Alert, {alertStyles} from 'sentry/components/alert';
  9. import ExternalLink from 'sentry/components/links/externalLink';
  10. import LoadingError from 'sentry/components/loadingError';
  11. import {PlatformKey} from 'sentry/data/platformCategories';
  12. import platforms from 'sentry/data/platforms';
  13. import {t, tct} from 'sentry/locale';
  14. import space from 'sentry/styles/space';
  15. import {Organization} from 'sentry/types';
  16. import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent';
  17. import getDynamicText from 'sentry/utils/getDynamicText';
  18. import {Theme} from 'sentry/utils/theme';
  19. import withApi from 'sentry/utils/withApi';
  20. import withOrganization from 'sentry/utils/withOrganization';
  21. import FirstEventFooter from './components/firstEventFooter';
  22. import FullIntroduction from './components/fullIntroduction';
  23. import {StepProps} from './types';
  24. /**
  25. * The documentation will include the following string should it be missing the
  26. * verification example, which currently a lot of docs are.
  27. */
  28. const INCOMPLETE_DOC_FLAG = 'TODO-ADD-VERIFICATION-EXAMPLE';
  29. type Props = StepProps & {
  30. api: Client;
  31. organization: Organization;
  32. };
  33. type State = {
  34. hasError: boolean;
  35. loadedPlatform: PlatformKey | null;
  36. platformDocs: {html: string; link: string} | null;
  37. };
  38. class DocumentationSetup extends Component<Props, State> {
  39. state: State = {
  40. platformDocs: null,
  41. loadedPlatform: null,
  42. hasError: false,
  43. };
  44. componentDidMount() {
  45. this.fetchData();
  46. }
  47. componentDidUpdate(nextProps: Props) {
  48. if (
  49. nextProps.platform !== this.props.platform ||
  50. nextProps.project !== this.props.project
  51. ) {
  52. this.fetchData();
  53. }
  54. }
  55. fetchData = async () => {
  56. const {api, project, organization, platform} = this.props;
  57. if (!project || !platform) {
  58. return;
  59. }
  60. try {
  61. const platformDocs = await loadDocs(api, organization.slug, project.slug, platform);
  62. this.setState({platformDocs, loadedPlatform: platform, hasError: false});
  63. } catch (error) {
  64. this.setState({hasError: error});
  65. throw error;
  66. }
  67. };
  68. handleFullDocsClick = () => {
  69. const {organization} = this.props;
  70. trackAdvancedAnalyticsEvent('growth.onboarding_view_full_docs', {organization});
  71. };
  72. /**
  73. * TODO(epurkhiser): This can be removed once all documentation has an
  74. * example for sending the users first event.
  75. */
  76. get missingExampleWarning() {
  77. const {loadedPlatform, platformDocs} = this.state;
  78. const missingExample =
  79. platformDocs && platformDocs.html.includes(INCOMPLETE_DOC_FLAG);
  80. if (!missingExample) {
  81. return null;
  82. }
  83. return (
  84. <Alert type="warning" showIcon>
  85. {tct(
  86. `Looks like this getting started example is still undergoing some
  87. work and doesn't include an example for triggering an event quite
  88. yet. If you have trouble sending your first event be sure to consult
  89. the [docsLink:full documentation] for [platform].`,
  90. {
  91. docsLink: <ExternalLink href={platformDocs?.link} />,
  92. platform: platforms.find(p => p.id === loadedPlatform)?.name,
  93. }
  94. )}
  95. </Alert>
  96. );
  97. }
  98. render() {
  99. const {organization, project, platform} = this.props;
  100. const {loadedPlatform, platformDocs, hasError} = this.state;
  101. const currentPlatform = loadedPlatform ?? platform ?? 'other';
  102. const docs = platformDocs !== null && (
  103. <DocsWrapper key={platformDocs.html}>
  104. <Content dangerouslySetInnerHTML={{__html: platformDocs.html}} />
  105. {this.missingExampleWarning}
  106. {project && (
  107. <FirstEventFooter
  108. project={project}
  109. organization={organization}
  110. docsLink={platformDocs?.link}
  111. docsOnClick={this.handleFullDocsClick}
  112. />
  113. )}
  114. </DocsWrapper>
  115. );
  116. const loadingError = (
  117. <LoadingError
  118. message={t('Failed to load documentation for the %s platform.', platform)}
  119. onRetry={this.fetchData}
  120. />
  121. );
  122. const testOnlyAlert = (
  123. <Alert type="warning">
  124. Platform documentation is not rendered in for tests in CI
  125. </Alert>
  126. );
  127. return (
  128. <Fragment>
  129. <FullIntroduction currentPlatform={currentPlatform} />
  130. {getDynamicText({
  131. value: !hasError ? docs : loadingError,
  132. fixed: testOnlyAlert,
  133. })}
  134. </Fragment>
  135. );
  136. }
  137. }
  138. type AlertType = React.ComponentProps<typeof Alert>['type'];
  139. const getAlertSelector = (type: AlertType) =>
  140. type === 'muted' ? null : `.alert[level="${type}"], .alert-${type}`;
  141. const mapAlertStyles = (p: {theme: Theme}, type: AlertType) =>
  142. css`
  143. ${getAlertSelector(type)} {
  144. ${alertStyles({theme: p.theme, type})};
  145. display: block;
  146. }
  147. `;
  148. const Content = styled(motion.div)`
  149. h1,
  150. h2,
  151. h3,
  152. h4,
  153. h5,
  154. h6,
  155. p {
  156. margin-bottom: 18px;
  157. }
  158. div[data-language] {
  159. margin-bottom: ${space(2)};
  160. }
  161. code {
  162. font-size: 87.5%;
  163. color: ${p => p.theme.pink300};
  164. }
  165. pre code {
  166. color: inherit;
  167. font-size: inherit;
  168. white-space: pre;
  169. }
  170. h2 {
  171. font-size: 1.4em;
  172. }
  173. .alert h5 {
  174. font-size: 1em;
  175. margin-bottom: 0.625rem;
  176. }
  177. /**
  178. * XXX(epurkhiser): This comes from the doc styles and avoids bottom margin issues in alerts
  179. */
  180. .content-flush-bottom *:last-child {
  181. margin-bottom: 0;
  182. }
  183. ${p => Object.keys(p.theme.alert).map(type => mapAlertStyles(p, type as AlertType))}
  184. `;
  185. const DocsWrapper = styled(motion.div)``;
  186. DocsWrapper.defaultProps = {
  187. initial: {opacity: 0, y: 40},
  188. animate: {opacity: 1, y: 0},
  189. exit: {opacity: 0},
  190. };
  191. export default withOrganization(withApi(DocumentationSetup));