cronsLandingPanel.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. import {useEffect} from 'react';
  2. import {browserHistory} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import onboardingImg from 'sentry-images/spot/onboarding-preview.svg';
  5. import {Button, LinkButton} from 'sentry/components/button';
  6. import ButtonBar from 'sentry/components/buttonBar';
  7. import OnboardingPanel from 'sentry/components/onboardingPanel';
  8. import Panel from 'sentry/components/panels/panel';
  9. import PanelBody from 'sentry/components/panels/panelBody';
  10. import {TabList, TabPanels, Tabs} from 'sentry/components/tabs';
  11. import {IconChevron} from 'sentry/icons';
  12. import {t} from 'sentry/locale';
  13. import {space} from 'sentry/styles/space';
  14. import {trackAnalytics} from 'sentry/utils/analytics';
  15. import {decodeScalar} from 'sentry/utils/queryString';
  16. import {useLocation} from 'sentry/utils/useLocation';
  17. import useOrganization from 'sentry/utils/useOrganization';
  18. import {normalizeUrl} from 'sentry/utils/withDomainRequired';
  19. import MonitorForm from 'sentry/views/monitors/components/monitorForm';
  20. import {Monitor} from 'sentry/views/monitors/types';
  21. import {NewMonitorButton} from './newMonitorButton';
  22. import {
  23. CRON_SDK_PLATFORMS,
  24. PlatformPickerPanel,
  25. SupportedPlatform,
  26. } from './platformPickerPanel';
  27. import {
  28. CeleryBeatAutoDiscovery,
  29. GoUpsertPlatformGuide,
  30. LaravelUpsertPlatformGuide,
  31. NodeJsUpsertPlatformGuide,
  32. PHPUpsertPlatformGuide,
  33. QuickStartProps,
  34. } from './quickStartEntries';
  35. enum GuideKey {
  36. BEAT_AUTO = 'beat_auto',
  37. UPSERT = 'upsert',
  38. MANUAL = 'manual',
  39. }
  40. interface PlatformGuide {
  41. Guide: React.ComponentType<QuickStartProps>;
  42. key: GuideKey;
  43. title: string;
  44. }
  45. const platformGuides: Record<SupportedPlatform, PlatformGuide[]> = {
  46. 'python-celery': [
  47. {
  48. Guide: CeleryBeatAutoDiscovery,
  49. title: 'Beat Auto Discovery',
  50. key: GuideKey.BEAT_AUTO,
  51. },
  52. ],
  53. php: [
  54. {
  55. Guide: PHPUpsertPlatformGuide,
  56. title: 'Upsert',
  57. key: GuideKey.UPSERT,
  58. },
  59. ],
  60. 'php-laravel': [
  61. {
  62. Guide: LaravelUpsertPlatformGuide,
  63. title: 'Upsert',
  64. key: GuideKey.UPSERT,
  65. },
  66. ],
  67. python: [],
  68. node: [
  69. {
  70. Guide: NodeJsUpsertPlatformGuide,
  71. title: 'Upsert',
  72. key: GuideKey.UPSERT,
  73. },
  74. ],
  75. go: [
  76. {
  77. Guide: GoUpsertPlatformGuide,
  78. title: 'Upsert',
  79. key: GuideKey.UPSERT,
  80. },
  81. ],
  82. };
  83. function isValidPlatform(platform?: string | null): platform is SupportedPlatform {
  84. return !!(platform && platform in platformGuides);
  85. }
  86. function isValidGuide(guide?: string): guide is GuideKey {
  87. return !!(guide && Object.values<string>(GuideKey).includes(guide));
  88. }
  89. export function CronsLandingPanel() {
  90. const organization = useOrganization();
  91. const location = useLocation();
  92. const platform = decodeScalar(location.query?.platform) ?? null;
  93. const guide = decodeScalar(location.query?.guide);
  94. useEffect(() => {
  95. if (!platform || !guide) {
  96. return;
  97. }
  98. trackAnalytics('landing_page.platform_guide.viewed', {
  99. organization,
  100. platform,
  101. guide,
  102. });
  103. }, [organization, platform, guide]);
  104. const navigateToPlatformGuide = (
  105. selectedPlatform: SupportedPlatform | null,
  106. selectedGuide?: string
  107. ) => {
  108. if (!selectedPlatform) {
  109. browserHistory.push({
  110. pathname: location.pathname,
  111. query: {...location.query, platform: undefined, guide: undefined},
  112. });
  113. return;
  114. }
  115. if (!selectedGuide) {
  116. selectedGuide = platformGuides[selectedPlatform][0]?.key ?? GuideKey.MANUAL;
  117. }
  118. browserHistory.push({
  119. pathname: location.pathname,
  120. query: {...location.query, platform: selectedPlatform, guide: selectedGuide},
  121. });
  122. };
  123. if (!isValidPlatform(platform) || !isValidGuide(guide)) {
  124. return <PlatformPickerPanel onSelect={navigateToPlatformGuide} />;
  125. }
  126. const platformText = CRON_SDK_PLATFORMS.find(
  127. ({platform: sdkPlatform}) => sdkPlatform === platform
  128. )?.label;
  129. const guides = platformGuides[platform];
  130. function onCreateMonitor(data: Monitor) {
  131. const url = normalizeUrl(`/organizations/${organization.slug}/crons/${data.slug}/`);
  132. browserHistory.push(url);
  133. }
  134. return (
  135. <Panel>
  136. <BackButton
  137. icon={<IconChevron size="sm" direction="left" />}
  138. onClick={() => navigateToPlatformGuide(null)}
  139. borderless
  140. >
  141. {t('Back to Platforms')}
  142. </BackButton>
  143. <PanelBody withPadding>
  144. <h3>{t('Get Started with %s', platformText)}</h3>
  145. <Tabs
  146. onChange={guideKey => navigateToPlatformGuide(platform, guideKey)}
  147. value={guide}
  148. >
  149. <TabList>
  150. {[
  151. ...guides.map(({key, title}) => (
  152. <TabList.Item key={key}>{title}</TabList.Item>
  153. )),
  154. <TabList.Item key={GuideKey.MANUAL}>{t('Manual')}</TabList.Item>,
  155. ]}
  156. </TabList>
  157. <TabPanels>
  158. {[
  159. ...guides.map(({key, Guide}) => (
  160. <TabPanels.Item key={key}>
  161. <GuideContainer>
  162. <Guide />
  163. </GuideContainer>
  164. </TabPanels.Item>
  165. )),
  166. <TabPanels.Item key={GuideKey.MANUAL}>
  167. <GuideContainer>
  168. <MonitorForm
  169. apiMethod="POST"
  170. apiEndpoint={`/organizations/${organization.slug}/monitors/`}
  171. onSubmitSuccess={onCreateMonitor}
  172. submitLabel={t('Next')}
  173. />
  174. </GuideContainer>
  175. </TabPanels.Item>,
  176. ]}
  177. </TabPanels>
  178. </Tabs>
  179. </PanelBody>
  180. </Panel>
  181. );
  182. }
  183. const BackButton = styled(Button)`
  184. font-weight: normal;
  185. color: ${p => p.theme.subText};
  186. margin: ${space(1)} 0 0 ${space(1)};
  187. padding-left: ${space(0.5)};
  188. padding-right: ${space(0.5)};
  189. `;
  190. const GuideContainer = styled('div')`
  191. display: flex;
  192. flex-direction: column;
  193. gap: ${space(2)};
  194. padding-top: ${space(2)};
  195. `;
  196. export function OldCronsLandingPanel() {
  197. return (
  198. <OnboardingPanel image={<img src={onboardingImg} />}>
  199. <h3>{t('Let Sentry monitor your recurring jobs')}</h3>
  200. <p>
  201. {t(
  202. "We'll tell you if your recurring jobs are running on schedule, failing, or succeeding."
  203. )}
  204. </p>
  205. <OnboardingActions gap={1}>
  206. <NewMonitorButton>{t('Set up first cron monitor')}</NewMonitorButton>
  207. <LinkButton href="https://docs.sentry.io/product/crons" external>
  208. {t('Read docs')}
  209. </LinkButton>
  210. </OnboardingActions>
  211. </OnboardingPanel>
  212. );
  213. }
  214. const OnboardingActions = styled(ButtonBar)`
  215. grid-template-columns: repeat(auto-fit, minmax(130px, max-content));
  216. `;