documentationSetup.tsx 5.9 KB

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