Skip to content
Open
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
118 changes: 60 additions & 58 deletions static/app/views/onboarding/components/scmAlertFrequency.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {Input} from '@sentry/scraps/input';
import {Container, Grid, Stack} from '@sentry/scraps/layout';
import {Flex, Grid, Stack} from '@sentry/scraps/layout';
import {Select} from '@sentry/scraps/select';
import {Text} from '@sentry/scraps/text';

import {IconInfo} from 'sentry/icons/iconInfo';
import {t} from 'sentry/locale';
import {ScmAlertOptionCard} from 'sentry/views/onboarding/components/scmAlertOptionCard';
import {
Expand Down Expand Up @@ -31,72 +32,73 @@ export function ScmAlertFrequency({
const isLaterSelected = alertSetting === RuleAction.CREATE_ALERT_LATER;

return (
<Stack gap="xl" role="radiogroup" aria-label={t('Alert frequency')}>
<ScmAlertOptionCard
label={t('High priority issues')}
isSelected={isDefaultSelected}
onSelect={() => onFieldChange('alertSetting', RuleAction.DEFAULT_ALERT)}
/>
<Stack gap="lg">
<Stack gap="md" role="radiogroup" aria-label={t('Alert frequency')}>
<ScmAlertOptionCard
label={t('High priority issues')}
description={t('Alert on new, regressed, and escalating issues')}
isSelected={isDefaultSelected}
onSelect={() => onFieldChange('alertSetting', RuleAction.DEFAULT_ALERT)}
/>

<ScmAlertOptionCard
label={t('Custom')}
isSelected={isCustomSelected}
onSelect={() => onFieldChange('alertSetting', RuleAction.CUSTOMIZED_ALERTS)}
>
<Container paddingLeft="2xl">
<Stack
gap="lg"
padding="sm 0 0 2xl"
borderLeft={isCustomSelected ? 'accent' : 'secondary'}
>
<Stack gap="xs">
<Container>
<ScmAlertOptionCard
label={t('Custom threshold')}
Comment thread
cursor[bot] marked this conversation as resolved.
isSelected={isCustomSelected}
onSelect={() => onFieldChange('alertSetting', RuleAction.CUSTOMIZED_ALERTS)}
>
{isCustomSelected && (
<Stack gap="lg">
<Stack gap="xs">
<Text size="md" density="comfortable">
{t('When there are more than')}
</Text>
</Container>
<Grid gap="md" columns=".35fr .65fr">
<Input
size="sm"
type="number"
min="0"
placeholder="10"
value={threshold}
onChange={e => onFieldChange('threshold', e.target.value)}
disabled={!isCustomSelected}
/>
<Select
size="sm"
value={metric}
options={METRIC_CHOICES}
onChange={option => onFieldChange('metric', option.value)}
disabled={!isCustomSelected}
/>
</Grid>
</Stack>
<Stack gap="xs">
<Container>
<Grid gap="xl" columns={{sm: '1fr', md: '1fr 1fr'}}>
<Input
size="md"
type="number"
min="0"
placeholder="10"
value={threshold}
onChange={e => onFieldChange('threshold', e.target.value)}
/>
<Select
size="md"
value={metric}
options={METRIC_CHOICES}
onChange={option => onFieldChange('metric', option.value)}
menuPortalTarget={document.body}
/>
</Grid>
</Stack>
<Stack gap="xs">
<Text size="md" density="comfortable">
{t('a unique error in')}
</Text>
</Container>
<Select
size="sm"
value={interval}
options={INTERVAL_CHOICES}
onChange={option => onFieldChange('interval', option.value)}
disabled={!isCustomSelected}
/>
<Select
size="md"
value={interval}
options={INTERVAL_CHOICES}
onChange={option => onFieldChange('interval', option.value)}
menuPortalTarget={document.body}
/>
</Stack>
</Stack>
</Stack>
</Container>
</ScmAlertOptionCard>
)}
</ScmAlertOptionCard>

<ScmAlertOptionCard
label={t("I'll set up alerts later")}
isSelected={isLaterSelected}
onSelect={() => onFieldChange('alertSetting', RuleAction.CREATE_ALERT_LATER)}
/>
</Stack>

<ScmAlertOptionCard
label={t("I'll create my own alerts later")}
isSelected={isLaterSelected}
onSelect={() => onFieldChange('alertSetting', RuleAction.CREATE_ALERT_LATER)}
/>
<Flex gap="sm" align="center">
<IconInfo size="md" variant="secondary" />
<Text variant="secondary" size="md" density="comfortable">
{t('You can always change alerts after project creation')}
</Text>
</Flex>
</Stack>
);
}
74 changes: 60 additions & 14 deletions static/app/views/onboarding/components/scmAlertOptionCard.tsx
Original file line number Diff line number Diff line change
@@ -1,36 +1,82 @@
import {Grid, Stack} from '@sentry/scraps/layout';
import styled from '@emotion/styled';

import {Container, Flex, Grid, Stack} from '@sentry/scraps/layout';
import {Radio} from '@sentry/scraps/radio';
import {Text} from '@sentry/scraps/text';

import {ScmCardButton} from 'sentry/views/onboarding/components/scmCardButton';
import {ScmCollapsibleReveal} from 'sentry/views/onboarding/components/scmCollapsibleReveal';
import {ScmSelectableContainer} from 'sentry/views/onboarding/components/scmSelectableContainer';

interface ScmAlertOptionCardProps {
isSelected: boolean;
label: string;
onSelect: () => void;
children?: React.ReactNode;
description?: string;
}

export function ScmAlertOptionCard({
label,
description,
isSelected,
onSelect,
children,
}: ScmAlertOptionCardProps) {
return (
<Stack gap="lg">
<ScmCardButton role="radio" aria-checked={isSelected} onClick={onSelect}>
<ScmSelectableContainer isSelected={isSelected} padding="lg">
<Grid gap="md" align="center" columns="min-content 1fr">
<Radio size="sm" readOnly checked={isSelected} tabIndex={-1} />
<Text bold={isSelected} size="md" density="comfortable">
{label}
</Text>
</Grid>
</ScmSelectableContainer>
</ScmCardButton>
{children}
</Stack>
<ScmSelectableContainer isSelected={isSelected}>
<Stack gap="0">
{/* The padding lives on the button (not the card) so the whole header,
edge to edge, is part of the click target. */}
<ScmCardButton
role="radio"
aria-checked={isSelected}
onClick={onSelect}
style={{width: '100%'}}
>
<Container padding="lg">
<Grid
columns="min-content 1fr"
gap="0 md"
areas={`
"radio label"
". description"
`}
>
<Flex area="radio" align="center">
<Radio size="xs" readOnly checked={isSelected} tabIndex={-1} />
</Flex>
<Container area="label">
<Text bold size="sm" density="comfortable">
{label}
</Text>
</Container>
{description && (
<Container area="description">
<Text variant="secondary" size="sm" density="comfortable">
{description}
</Text>
</Container>
)}
</Grid>
</Container>
</ScmCardButton>
{/* Selecting the card expands its body; ScmCollapsibleReveal's height
tween lets cards in scmCreateProject's layout="position" group
reflow smoothly. */}
<ScmCollapsibleReveal open={Boolean(children)}>
<ExpandedBody>{children}</ExpandedBody>
</ScmCollapsibleReveal>
Comment thread
sentry[bot] marked this conversation as resolved.
</Stack>
</ScmSelectableContainer>
);
}

// The body indents to line up under the label (header button padding + the xs
// radio's 12px width + grid column gap) and insets its right/bottom to match
// the header's padding, since the card itself carries none. The top gap comes
// from the header button's own bottom padding.
const ExpandedBody = styled('div')`
padding: 0 ${p => p.theme.space.lg} ${p => p.theme.space.lg};
padding-left: calc(${p => p.theme.space.lg} + 12px + ${p => p.theme.space.md});
`;
51 changes: 51 additions & 0 deletions static/app/views/onboarding/components/scmCollapsibleReveal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import {useState} from 'react';
import {AnimatePresence, motion} from 'framer-motion';

interface ScmCollapsibleRevealProps {
children: React.ReactNode;
/** When true the content is shown; toggling tweens height and opacity. */
open: boolean;
/** Forwarded to the animated element, e.g. as an aria-controls target. */
id?: string;
}

/**
* Reveals or hides content with a height + fade tween. Shared by
* ScmCollapsibleSection and ScmAlertOptionCard so their expand/collapse timing
* stays in sync. Animating height (rather than display) lets sibling cards in a
* framer-motion layout="position" group reflow via normal document flow.
* initial={false} renders the open state without animating on mount.
*/
export function ScmCollapsibleReveal({open, id, children}: ScmCollapsibleRevealProps) {
// overflow:hidden is needed while the height tween runs so the content clips
// cleanly, but kept on it would also clip anything that extends past the
// settled bounds, e.g. a focus ring at the edge or an open select menu below.
// Switch to visible once open and settled, back to hidden whenever animating.
const [overflow, setOverflow] = useState<'hidden' | 'visible'>(
open ? 'visible' : 'hidden'
);
Comment thread
sentry[bot] marked this conversation as resolved.

return (
<AnimatePresence initial={false}>
{open && (
<motion.div
key="content"
id={id}
initial={{height: 0, opacity: 0}}
animate={{height: 'auto', opacity: 1}}
exit={{height: 0, opacity: 0}}
transition={{duration: 0.2, ease: 'easeOut'}}
onAnimationStart={() => setOverflow('hidden')}
onAnimationComplete={() => {
if (open) {
setOverflow('visible');
}
}}
style={{overflow, width: '100%'}}
>
{children}
</motion.div>
)}
</AnimatePresence>
);
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import {useId, useState} from 'react';
import styled from '@emotion/styled';
import {AnimatePresence, motion} from 'framer-motion';

import {Button} from '@sentry/scraps/button';
import {Flex, Stack} from '@sentry/scraps/layout';
import {Text} from '@sentry/scraps/text';

import {IconChevron} from 'sentry/icons';
import {ScmCollapsibleReveal} from 'sentry/views/onboarding/components/scmCollapsibleReveal';

interface ScmCollapsibleSectionProps {
children: React.ReactNode;
Expand Down Expand Up @@ -64,21 +64,9 @@ export function ScmCollapsibleSection({
</ToggleButton>
{trailing}
</Flex>
<AnimatePresence initial={false}>
{expanded && (
<motion.div
id={contentId}
key="content"
initial={{height: 0, opacity: 0}}
animate={{height: 'auto', opacity: 1}}
exit={{height: 0, opacity: 0}}
transition={{duration: 0.2, ease: 'easeOut'}}
style={{overflow: 'hidden', width: '100%'}}
>
<Content width="100%">{children}</Content>
</motion.div>
)}
</AnimatePresence>
<ScmCollapsibleReveal open={expanded} id={contentId}>
<Content width="100%">{children}</Content>
</ScmCollapsibleReveal>
</Stack>
);
}
Expand Down
10 changes: 8 additions & 2 deletions static/app/views/onboarding/scmProjectDetails.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,14 @@ describe('ScmProjectDetails', () => {
});

expect(await screen.findByText('High priority issues')).toBeInTheDocument();
expect(screen.getByText('Custom')).toBeInTheDocument();
expect(screen.getByText("I'll create my own alerts later")).toBeInTheDocument();
expect(
screen.getByText('Alert on new, regressed, and escalating issues')
).toBeInTheDocument();
expect(screen.getByText('Custom threshold')).toBeInTheDocument();
expect(screen.getByText("I'll set up alerts later")).toBeInTheDocument();
expect(
screen.getByText('You can always change alerts after project creation')
).toBeInTheDocument();
});

it('re-derives the fields when the host clears the form', () => {
Expand Down
2 changes: 1 addition & 1 deletion static/app/views/projectInstall/scmCreateProject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ function ScmCreateProjectWizard({initialState}: {initialState: WizardState}) {
>
<Layout.Title>{t('Create a new project')}</Layout.Title>

<MotionStack paddingBottom="lg" gap="md" layout="position">
<MotionStack gap="md" layout="position">
<Heading as="h1">{t('Create a project')}</Heading>
<Text variant="secondary" density="comfortable">
{tct(
Expand Down
8 changes: 3 additions & 5 deletions tests/acceptance/test_scm_onboarding.py
Original file line number Diff line number Diff line change
Expand Up @@ -744,12 +744,10 @@ def test_scm_back_from_setup_docs_active_project_alert_changed(self) -> None:
self.browser.click('[aria-label="Back"]')
self.browser.wait_until('[data-test-id="onboarding-step-scm-project-details"]')

# Switch alerts from "High priority issues" to "create later".
self.browser.click(
xpath='//button[@role="radio"][contains(., "create my own alerts later")]'
)
# Switch alerts from "High priority issues" to "set up later".
self.browser.click(xpath='//button[@role="radio"][contains(., "set up alerts later")]')
self.browser.wait_until(
xpath='//button[@role="radio"][@aria-checked="true"][contains(., "create my own alerts later")]'
xpath='//button[@role="radio"][@aria-checked="true"][contains(., "set up alerts later")]'
)
self.browser.wait_until_clickable(xpath='//button[contains(., "Create project")]')
self.browser.click(xpath='//button[contains(., "Create project")]')
Expand Down
Loading