Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@ const pythonPlatform: OnboardingSelectedSDK = {
category: 'popular',
};

// In neither platformProductAvailability nor PLATFORM_PRODUCT_INFO, so the
// section has nothing to configure.
const platformWithoutProducts: OnboardingSelectedSDK = {
key: 'other',
name: 'Other',
language: 'other',
type: 'language',
link: 'https://docs.sentry.io/platforms/',
category: 'popular',
};

function defaultProps(overrides: Partial<Record<string, unknown>> = {}) {
return {
analyticsFlow: 'onboarding' as const,
Expand Down Expand Up @@ -61,4 +72,67 @@ describe('ScmFeatureSelectionPanel', () => {
expect(screen.queryByText(/unlimited volume for 14 days/)).not.toBeInTheDocument();
expect(screen.queryByText('5,000 errors / mo')).not.toBeInTheDocument();
});

it('prompts for a platform without showing cards in project creation', async () => {
render(
<ScmFeatureSelectionPanel
{...defaultProps({
analyticsFlow: 'project-creation',
selectedPlatform: undefined,
})}
/>,
{organization}
);

expect(await screen.findByText('Products')).toBeInTheDocument();
expect(
screen.getByText('Select a platform to configure products')
).toBeInTheDocument();
expect(screen.queryByRole('checkbox', {name: /Tracing/})).not.toBeInTheDocument();
});

it('reveals the cards once a platform with products is chosen', async () => {
const {rerender} = render(
<ScmFeatureSelectionPanel
{...defaultProps({
analyticsFlow: 'project-creation',
selectedPlatform: undefined,
})}
/>,
{organization}
);

expect(await screen.findByText('Products')).toBeInTheDocument();
expect(screen.queryByRole('checkbox', {name: /Tracing/})).not.toBeInTheDocument();

rerender(
<ScmFeatureSelectionPanel
{...defaultProps({
analyticsFlow: 'project-creation',
selectedPlatform: pythonPlatform,
})}
/>
);

expect(await screen.findByRole('checkbox', {name: /Tracing/})).toBeInTheDocument();
expect(
screen.queryByText('Select a platform to configure products')
).not.toBeInTheDocument();
});

it('drops the section and its trailing divider for a platform with no products', () => {
render(
<ScmFeatureSelectionPanel
{...defaultProps({
analyticsFlow: 'project-creation',
selectedPlatform: platformWithoutProducts,
trailing: <div>Trailing divider</div>,
})}
/>,
{organization}
);

expect(screen.queryByText('Products')).not.toBeInTheDocument();
expect(screen.queryByText('Trailing divider')).not.toBeInTheDocument();
});
});
170 changes: 95 additions & 75 deletions static/app/components/onboarding/scm/scmFeatureSelectionPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {Fragment, type ReactNode, useCallback, useMemo} from 'react';
import {type ReactNode, useCallback, useMemo} from 'react';
import {motion} from 'framer-motion';

import {Tag} from '@sentry/scraps/badge';
import {Flex, Stack} from '@sentry/scraps/layout';
import {Container, Flex, Stack} from '@sentry/scraps/layout';
import {Heading, Text} from '@sentry/scraps/text';

import {ProductSolution} from 'sentry/components/onboarding/gettingStartedDoc/types';
Expand All @@ -19,6 +19,7 @@ import {trackAnalytics} from 'sentry/utils/analytics';
import {useOrganization} from 'sentry/utils/useOrganization';

import {type ScmAnalyticsFlow, scmFlowVariantParams} from './scmAnalyticsFlow';
import {ScmCollapsibleReveal} from './scmCollapsibleReveal';
import {ScmFeatureInfoCards} from './scmFeatureInfoCards';
import {ScmFeatureSelectionCards} from './scmFeatureSelectionCards';
import {
Expand All @@ -40,9 +41,10 @@ interface ScmFeatureSelectionPanelProps {
selectedFeatures: ProductSolution[] | undefined;
selectedPlatform: OnboardingSelectedSDK | undefined;
selectedRepository: Repository | undefined;
// Optional element rendered as a sibling after the panel content (e.g. a
// divider from the host). Dropped together with the panel when there is
// nothing to show, so the host never strands an orphaned divider.
// Optional element rendered after the panel content (e.g. a divider from the
// host). Lives inside the panel's reveal, so it is dropped — and tweens away
// — together with the panel when there is nothing to show, and the host never
// strands an orphaned divider.
trailing?: ReactNode;
}

Expand Down Expand Up @@ -168,81 +170,99 @@ export function ScmFeatureSelectionPanel({
]
);

// Hide the whole section when a resolved platform has no configurable
const hasFeatureCards = featureMode !== 'none';

// Show the whole section unless a resolved platform has no configurable
// products. Before a platform is chosen (no resolved key), keep it visible in
// project creation for the select-a-platform prompt; onboarding hides both.
if (featureMode === 'none' && (isOnboarding || !!currentPlatformKey)) {
return null;
}
// Expressed as a flag rather than an early `return null` so the reveal below
// stays mounted and can tween the section in and out. AnimatePresence's
// `initial={false}` still renders the settled state on first mount, so a page
// load with a platform already resolved does not animate.
const showSection = hasFeatureCards || (!isOnboarding && !currentPlatformKey);

return (
<Fragment>
<MotionStack layout="position" width="100%">
<Stack
gap={isOnboarding ? '2xl' : 'lg'}
paddingTop={isOnboarding ? 'xs' : undefined}
>
{isOnboarding ? (
<Flex
padding="lg"
background="secondary"
border="secondary"
radius="md"
gap="lg"
>
<IconBusiness size="lg" variant="accent" />
<Text size="md" density="comfortable">
{tct(
'You’ve got [bold:unlimited volume for 14 days] to try out everything. After that, free plan volumes apply ⋅ No credit card required',
{
bold: (
<Text as="span" bold variant="accent">
{null}
</Text>
),
}
<ScmCollapsibleReveal open={showSection}>
{/* No gap: the section claims no spacing of its own, so everything that
tweens away on collapse is inside this box. Hosts own the rhythm —
the section above supplies the space before it, and `trailing`
carries the space around itself. */}
<Stack gap="0" width="100%">
<MotionStack layout="position" width="100%">
{/* gap="0" because the spacing above the cards has to tween with
them: a flex gap would snap in at full size while the revealed
height is still 0, jumping the layout by that much. The cards own
it as padding inside the reveal instead. */}
<Stack gap="0" paddingTop={isOnboarding ? 'xs' : undefined}>
{isOnboarding ? (
<Flex
padding="lg"
background="secondary"
border="secondary"
radius="md"
gap="lg"
>
<IconBusiness size="lg" variant="accent" />
<Text size="md" density="comfortable">
{tct(
'You’ve got [bold:unlimited volume for 14 days] to try out everything. After that, free plan volumes apply ⋅ No credit card required',
{
bold: (
<Text as="span" bold variant="accent">
{null}
</Text>
),
}
)}
</Text>
</Flex>
) : null}

{isOnboarding ? null : (
<Flex justify="between" align="center" gap="md">
<Heading as="h4">{t('Products')}</Heading>
{currentPlatformKey ? null : (
<Tag variant="muted" icon={<IconInfo />} style={{minWidth: 0}}>
<Text ellipsis variant="inherit">
{t('Select a platform to configure products')}
</Text>
</Tag>
)}
</Text>
</Flex>
) : null}

{isOnboarding ? null : (
<Flex justify="between" align="center" gap="md">
<Heading as="h4">{t('Products')}</Heading>
{currentPlatformKey ? null : (
<Tag variant="muted" icon={<IconInfo />} style={{minWidth: 0}}>
<Text ellipsis variant="inherit">
{t('Select a platform to configure products')}
</Text>
</Tag>
)}
</Flex>
)}

{featureMode === 'toggleable' ? (
<ScmFeatureSelectionCards
availableFeatures={availableFeatures}
selectedFeatures={currentFeatures}
disabledProducts={disabledProducts}
onToggleFeature={handleToggleFeature}
featureMeta={featureMeta}
isVolumeLoading={isFeatureMetaLoading}
isOnboarding={isOnboarding}
/>
) : featureMode === 'informational' ? (
<ScmFeatureInfoCards
availableFeatures={availableFeatures}
disabledProducts={disabledProducts}
featureMeta={featureMeta}
platformName={currentPlatformName}
isVolumeLoading={isFeatureMetaLoading}
isOnboarding={isOnboarding}
/>
) : null}
</Stack>
</MotionStack>
{trailing}
</Fragment>
</Flex>
)}

{/* The cards mount at their full height, so without a clipped
height tween they paint over the sections below while
framer-motion is still animating those into place. */}
<ScmCollapsibleReveal open={hasFeatureCards}>
<Container paddingTop={isOnboarding ? '2xl' : 'lg'}>
{featureMode === 'toggleable' ? (
<ScmFeatureSelectionCards
availableFeatures={availableFeatures}
selectedFeatures={currentFeatures}
disabledProducts={disabledProducts}
onToggleFeature={handleToggleFeature}
featureMeta={featureMeta}
isVolumeLoading={isFeatureMetaLoading}
isOnboarding={isOnboarding}
/>
) : featureMode === 'informational' ? (
<ScmFeatureInfoCards
availableFeatures={availableFeatures}
disabledProducts={disabledProducts}
featureMeta={featureMeta}
platformName={currentPlatformName}
isVolumeLoading={isFeatureMetaLoading}
isOnboarding={isOnboarding}
/>
) : null}
</Container>
</ScmCollapsibleReveal>
</Stack>
</MotionStack>
{trailing}
</Stack>
</ScmCollapsibleReveal>
);
}

Expand Down
Loading
Loading