import {Fragment, useCallback, useEffect, useMemo} from 'react'; import {css} from '@emotion/react'; import styled from '@emotion/styled'; import {Alert} from 'sentry/components/alert'; import {Button} from 'sentry/components/button'; import Checkbox from 'sentry/components/checkbox'; import ExternalLink from 'sentry/components/links/externalLink'; import {Tooltip} from 'sentry/components/tooltip'; import {PlatformKey} from 'sentry/data/platformCategories'; import {IconQuestion} from 'sentry/icons'; import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import {decodeList} from 'sentry/utils/queryString'; import useRouter from 'sentry/utils/useRouter'; import TextBlock from 'sentry/views/settings/components/text/textBlock'; export enum ProductSolution { ERROR_MONITORING = 'error-monitoring', PERFORMANCE_MONITORING = 'performance-monitoring', SESSION_REPLAY = 'session-replay', PROFILING = 'profiling', } // This is the list of products that are available for each platform // Since the ProductSelection component is rendered in the onboarding/project creation flow only, it is ok to have this list here // NOTE: Please keep the prefix in alphabetical order export const platformProductAvailability = { javascript: [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.SESSION_REPLAY], 'javascript-react': [ ProductSolution.PERFORMANCE_MONITORING, ProductSolution.SESSION_REPLAY, ], 'javascript-vue': [ ProductSolution.PERFORMANCE_MONITORING, ProductSolution.SESSION_REPLAY, ], 'javascript-angular': [ ProductSolution.PERFORMANCE_MONITORING, ProductSolution.SESSION_REPLAY, ], 'javascript-ember': [ ProductSolution.PERFORMANCE_MONITORING, ProductSolution.SESSION_REPLAY, ], 'javascript-gatsby': [ ProductSolution.PERFORMANCE_MONITORING, ProductSolution.SESSION_REPLAY, ], 'javascript-nextjs': [ ProductSolution.PERFORMANCE_MONITORING, ProductSolution.SESSION_REPLAY, ], 'javascript-remix': [ ProductSolution.PERFORMANCE_MONITORING, ProductSolution.SESSION_REPLAY, ], 'javascript-svelte': [ ProductSolution.PERFORMANCE_MONITORING, ProductSolution.SESSION_REPLAY, ], 'javascript-sveltekit': [ ProductSolution.PERFORMANCE_MONITORING, ProductSolution.SESSION_REPLAY, ], node: [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], 'node-connect': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], python: [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], 'python-aiohttp': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], 'python-awslambda': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], 'python-bottle': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], 'python-celery': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], 'python-chalice': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], 'python-django': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], 'python-falcon': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], 'python-fastapi': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], 'python-flask': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], 'python-gcpfunctions': [ ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING, ], 'python-pyramid': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], 'python-quart': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], 'python-rq': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], 'python-sanic': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], 'python-serverless': [ ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING, ], 'python-tornado': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], 'python-starlette': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], 'python-tryton': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], 'python-wsgi': [ProductSolution.PERFORMANCE_MONITORING, ProductSolution.PROFILING], } as Record; export type DisabledProduct = { reason: string; onClick?: () => void; product?: ProductSolution; }; type ProductProps = { /** * If the product is checked. This information is grabbed from the URL. */ checked: boolean; /** * The name of the product */ label: string; /** * Brief product description */ description?: string; /** * If the product is disabled. It contains a reason and an optional onClick handler */ disabled?: DisabledProduct; /** * Link of the product documentation. Rendered if there is also a description. */ docLink?: string; /** * Click handler. If the product is enabled, by clicking on the button, the product is added or removed from the URL. */ onClick?: () => void; /** * A permanent disabled product is always disabled and cannot be enabled. */ permanentDisabled?: boolean; }; function Product({ disabled, permanentDisabled, checked, label, onClick, docLink, description, }: ProductProps) { const ProductWrapper = permanentDisabled ? PermanentDisabledProductWrapper : disabled ? DisabledProductWrapper : ProductButtonWrapper; return ( {description} {docLink && {t('Read the Docs')}} )) } isHoverable > {label} ); } export type ProductSelectionProps = { /** * List of products that are disabled. All of them have to contain a reason by default and optionally an onClick handler. */ disabledProducts?: DisabledProduct[]; /** * If true, the loader script is used instead of the npm/yarn guide. */ lazyLoader?: boolean; /** * The platform key of the project (e.g. javascript-react, python-django, etc.) */ platform?: PlatformKey; /** * A custom list of products per platform. If not provided, the default list is used. */ productsPerPlatform?: Record; skipLazyLoader?: () => void; /** * If true, the component has a bottom margin of 20px */ withBottomMargin?: boolean; }; export function ProductSelection({ disabledProducts, lazyLoader, skipLazyLoader, platform, withBottomMargin, productsPerPlatform = platformProductAvailability, }: ProductSelectionProps) { const router = useRouter(); const urlProducts = decodeList(router.location.query.product); const products: ProductSolution[] | undefined = platform ? productsPerPlatform[platform] : undefined; const defaultProducts = useMemo(() => { return ( products?.filter( product => !disabledProducts?.some(disabledProduct => disabledProduct.product === product) ) ?? [] ); }, [products, disabledProducts]); useEffect(() => { router.replace({ pathname: router.location.pathname, query: { ...router.location.query, product: defaultProducts, }, }); // Adding defaultProducts to the dependency array causes an max-depth error // eslint-disable-next-line react-hooks/exhaustive-deps }, [router]); const handleClickProduct = useCallback( (product: ProductSolution) => { let newProduct = urlProducts.includes(product) ? urlProducts.filter(p => p !== product) : [...urlProducts, product]; if (defaultProducts?.includes(ProductSolution.PROFILING)) { if ( !newProduct.includes(ProductSolution.PERFORMANCE_MONITORING) && newProduct.includes(ProductSolution.PROFILING) ) { newProduct = [...newProduct, ProductSolution.PERFORMANCE_MONITORING]; } } router.replace({ pathname: router.location.pathname, query: { ...router.location.query, product: newProduct, }, }); }, [router, urlProducts, defaultProducts] ); if (!products) { // if the platform does not support any product, we don't render anything return null; } // TODO(aknaus): clean up // The package manager info is only shown for javascript platforms // until we improve multi snippet suppport const showPackageManagerInfo = platform?.indexOf('javascript') === 0; return ( {showPackageManagerInfo && ( {lazyLoader ? tct('In this quick guide you’ll use our [loaderScript] to set up:', { loaderScript: Loader Script, }) : tct('In this quick guide you’ll use [npm] or [yarn] to set up:', { npm: npm, yarn: yarn, })} )} {products.includes(ProductSolution.PERFORMANCE_MONITORING) && ( handleClickProduct(ProductSolution.PERFORMANCE_MONITORING)} disabled={ urlProducts.includes(ProductSolution.PROFILING) ? { reason: t( 'You must have Performance Monitoring set up to use Profiling. Disabling it is not possible while Profiling is selected.' ), } : disabledProducts?.find( disabledProduct => disabledProduct.product === ProductSolution.PERFORMANCE_MONITORING ) } checked={urlProducts.includes(ProductSolution.PERFORMANCE_MONITORING)} permanentDisabled={urlProducts.includes(ProductSolution.PROFILING)} /> )} {products.includes(ProductSolution.SESSION_REPLAY) && ( handleClickProduct(ProductSolution.SESSION_REPLAY)} disabled={disabledProducts?.find( disabledProduct => disabledProduct.product === ProductSolution.SESSION_REPLAY )} checked={urlProducts.includes(ProductSolution.SESSION_REPLAY)} /> )} {products.includes(ProductSolution.PROFILING) && ( handleClickProduct(ProductSolution.PROFILING)} disabled={disabledProducts?.find( disabledProduct => disabledProduct.product === ProductSolution.PROFILING )} checked={urlProducts.includes(ProductSolution.PROFILING)} /> )} {showPackageManagerInfo && lazyLoader && ( {tct('Prefer to set up Sentry using [npm:npm] or [yarn:yarn]? [goHere].', { npm: , yarn: , goHere: ( ), })} )} ); } const Products = styled('div')` display: flex; flex-wrap: wrap; gap: ${space(1)}; `; const ProductButtonWrapper = styled(Button)` ${p => p.priority === 'primary' && css` &, :hover { background: ${p.theme.purple100}; color: ${p.theme.purple300}; } `} `; const DisabledProductWrapper = styled(Button)` && { cursor: ${p => (p.disabled ? 'not-allowed' : 'pointer')}; input { cursor: ${p => p.disabled || p.priority === 'default' ? 'not-allowed' : 'pointer'}; } } `; const PermanentDisabledProductWrapper = styled(Button)` && { &, :hover { background: ${p => p.theme.purple100}; color: ${p => p.theme.purple300}; opacity: 0.5; cursor: not-allowed; input { cursor: not-allowed; } } } `; const ProductButtonInner = styled('div')` display: grid; grid-template-columns: repeat(3, max-content); gap: ${space(1)}; align-items: center; `; const Divider = styled('hr')<{withBottomMargin?: boolean}>` height: 1px; width: 100%; background: ${p => p.theme.border}; border: none; ${p => p.withBottomMargin && `margin-bottom: ${space(3)}`} `; const TooltipDescription = styled('div')` display: flex; flex-direction: column; gap: ${space(0.5)}; justify-content: flex-start; `; const AlternativeInstallationAlert = styled(Alert)` margin-top: ${space(3)}; `;