cronsLandingPanel.tsx 6.6 KB

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