Skip to content

Commit 89c6efb

Browse files
author
Elias Hussary
authored
feat(replays): onboarding-v2 unsupported/supported project CTA states (#46526)
## Summary This PR adds the following: - alert banner if select project does not support replay - new CTA states - if all projects don't support replay - Create project (disabled if no permission) - if selected projects don't support replay - Disable button All _selected_ Projects don't support replay: ![image](https://user-images.githubusercontent.com/7349258/228635977-6575cebc-5eed-41bd-a602-224f6f4d48d7.png) All projects don't support replay: ![image](https://user-images.githubusercontent.com/7349258/228636593-7287c4a8-7153-456f-ad24-ab7053f55af9.png) All projects don't support replay, w/o create permissions: ![image](https://user-images.githubusercontent.com/7349258/228636882-9eabd491-f745-46cd-bf18-6670005f6b27.png) Relates to: #45474 (comment)
1 parent a27b354 commit 89c6efb

File tree

2 files changed

+172
-17
lines changed

2 files changed

+172
-17
lines changed

static/app/views/replays/list/replayOnboardingPanel.tsx

Lines changed: 139 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,22 @@ import styled from '@emotion/styled';
44
import emptyStateImg from 'sentry-images/spot/replays-empty-state.svg';
55

66
import Feature from 'sentry/components/acl/feature';
7+
import Alert from 'sentry/components/alert';
78
import {Button} from 'sentry/components/button';
89
import ButtonBar from 'sentry/components/buttonBar';
910
import HookOrDefault from 'sentry/components/hookOrDefault';
11+
import ExternalLink from 'sentry/components/links/externalLink';
1012
import OnboardingPanel from 'sentry/components/onboardingPanel';
11-
import {t} from 'sentry/locale';
13+
import {Tooltip} from 'sentry/components/tooltip';
14+
import {replayPlatforms} from 'sentry/data/platformCategories';
15+
import {IconInfo} from 'sentry/icons';
16+
import {t, tct} from 'sentry/locale';
1217
import PreferencesStore from 'sentry/stores/preferencesStore';
1318
import {useLegacyStore} from 'sentry/stores/useLegacyStore';
1419
import {useReplayOnboardingSidebarPanel} from 'sentry/utils/replays/hooks/useReplayOnboarding';
1520
import useOrganization from 'sentry/utils/useOrganization';
21+
import usePageFilters from 'sentry/utils/usePageFilters';
22+
import useProjects from 'sentry/utils/useProjects';
1623

1724
type Breakpoints = {
1825
large: string;
@@ -28,6 +35,34 @@ const OnboardingCTAHook = HookOrDefault({
2835

2936
export default function ReplayOnboardingPanel() {
3037
const preferences = useLegacyStore(PreferencesStore);
38+
const pageFilters = usePageFilters();
39+
const projects = useProjects();
40+
const organization = useOrganization();
41+
const canCreateProjects = organization.access.includes('project:admin');
42+
43+
const selectedProjects = projects.projects.filter(p =>
44+
pageFilters.selection.projects.includes(Number(p.id))
45+
);
46+
47+
const hasSelectedProjects = selectedProjects.length > 0;
48+
49+
const allProjectsUnsupported = projects.projects.every(
50+
p => !replayPlatforms.includes(p.platform!)
51+
);
52+
53+
const allSelectedProjectsUnsupported = selectedProjects.every(
54+
p => !replayPlatforms.includes(p.platform!)
55+
);
56+
57+
// if all projects are unsupported we should prompt the user to create a project
58+
// else we prompt to setup
59+
const primaryAction = allProjectsUnsupported ? 'create' : 'setup';
60+
// disable "create" if the user has insufficient permissions
61+
// disable "setup" if the current selected pageFilters are not supported
62+
const primaryActionDisabled =
63+
primaryAction === 'create'
64+
? !canCreateProjects
65+
: allSelectedProjectsUnsupported && hasSelectedProjects;
3166

3267
const breakpoints = preferences.collapsed
3368
? {
@@ -43,26 +78,115 @@ export default function ReplayOnboardingPanel() {
4378
xlarge: '1450px',
4479
};
4580

46-
const organization = useOrganization();
47-
4881
return (
49-
<OnboardingPanel image={<HeroImage src={emptyStateImg} breakpoints={breakpoints} />}>
50-
<Feature
51-
features={['session-replay-ga']}
52-
organization={organization}
53-
renderDisabled={() => <SetupReplaysCTA />}
82+
<Fragment>
83+
{hasSelectedProjects && allSelectedProjectsUnsupported && (
84+
<Alert icon={<IconInfo />}>
85+
{tct(
86+
`[projectMsg] [action] a project using our [link], or equivalent framework SDK.`,
87+
{
88+
action: primaryAction === 'create' ? t('Create') : t('Select'),
89+
projectMsg: (
90+
<strong>
91+
{t(
92+
`Session Replay isn't available for project %s.`,
93+
selectedProjects[0].slug
94+
)}
95+
</strong>
96+
),
97+
link: (
98+
<ExternalLink href="https://docs.sentry.io/platforms/javascript/session-replay/">
99+
{t('Sentry browser SDK package')}
100+
</ExternalLink>
101+
),
102+
}
103+
)}
104+
</Alert>
105+
)}
106+
<OnboardingPanel
107+
image={<HeroImage src={emptyStateImg} breakpoints={breakpoints} />}
54108
>
55-
<OnboardingCTAHook organization={organization}>
56-
<SetupReplaysCTA />
57-
</OnboardingCTAHook>
58-
</Feature>
59-
</OnboardingPanel>
109+
<Feature
110+
features={['session-replay-ga']}
111+
organization={organization}
112+
renderDisabled={() => (
113+
<SetupReplaysCTA
114+
orgSlug={organization.slug}
115+
primaryAction={primaryAction}
116+
disabled={primaryActionDisabled}
117+
/>
118+
)}
119+
>
120+
<OnboardingCTAHook organization={organization}>
121+
<SetupReplaysCTA
122+
orgSlug={organization.slug}
123+
primaryAction={primaryAction}
124+
disabled={primaryActionDisabled}
125+
/>
126+
</OnboardingCTAHook>
127+
</Feature>
128+
</OnboardingPanel>
129+
</Fragment>
60130
);
61131
}
62132

63-
function SetupReplaysCTA() {
133+
interface SetupReplaysCTAProps {
134+
orgSlug: string;
135+
primaryAction: 'setup' | 'create';
136+
disabled?: boolean;
137+
}
138+
139+
export function SetupReplaysCTA({
140+
disabled,
141+
primaryAction = 'setup',
142+
orgSlug,
143+
}: SetupReplaysCTAProps) {
64144
const {activateSidebar} = useReplayOnboardingSidebarPanel();
65145

146+
function renderCTA() {
147+
if (primaryAction === 'setup') {
148+
return (
149+
<Tooltip
150+
title={
151+
<span data-test-id="setup-replays-tooltip">
152+
{t('Select a supported project from the projects dropdown.')}
153+
</span>
154+
}
155+
disabled={!disabled} // we only want to show the tooltip when the button is disabled
156+
>
157+
<Button
158+
data-test-id="setup-replays-btn"
159+
onClick={activateSidebar}
160+
priority="primary"
161+
disabled={disabled}
162+
>
163+
{t('Set Up Replays')}
164+
</Button>
165+
</Tooltip>
166+
);
167+
}
168+
169+
return (
170+
<Tooltip
171+
title={
172+
<span data-test-id="create-project-tooltip">
173+
{t('Only admins, managers, and owners, can create projects.')}
174+
</span>
175+
}
176+
disabled={!disabled}
177+
>
178+
<Button
179+
data-test-id="create-project-btn"
180+
to={`/organizations/${orgSlug}/projects/new/`}
181+
priority="primary"
182+
disabled={disabled}
183+
>
184+
{t('Create Project')}
185+
</Button>
186+
</Tooltip>
187+
);
188+
}
189+
66190
return (
67191
<Fragment>
68192
<h3>{t('Get to the root cause faster')}</h3>
@@ -72,9 +196,7 @@ function SetupReplaysCTA() {
72196
)}
73197
</p>
74198
<ButtonList gap={1}>
75-
<Button onClick={activateSidebar} priority="primary">
76-
{t('Set Up Replays')}
77-
</Button>
199+
{renderCTA()}
78200
<Button
79201
href="https://docs.sentry.io/platforms/javascript/session-replay/"
80202
external
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
2+
3+
import {SetupReplaysCTA} from 'sentry/views/replays/list/replayOnboardingPanel';
4+
5+
describe('SetupReplaysCTA', () => {
6+
it('renders setup replay', () => {
7+
render(<SetupReplaysCTA primaryAction="setup" orgSlug="foo" />);
8+
expect(screen.getByTestId('setup-replays-btn')).toBeInTheDocument();
9+
});
10+
11+
it('renders setup replay w/ disabled state including tooltip', async () => {
12+
render(<SetupReplaysCTA primaryAction="setup" orgSlug="foo" disabled />);
13+
const setupBtn = screen.getByTestId('setup-replays-btn');
14+
await userEvent.hover(setupBtn);
15+
await waitFor(() => screen.getByTestId('setup-replays-tooltip'));
16+
expect(screen.getByTestId('setup-replays-tooltip')).toBeInTheDocument();
17+
});
18+
19+
it('create project', () => {
20+
render(<SetupReplaysCTA primaryAction="create" orgSlug="foo" />);
21+
const createBtn = screen.getByTestId('create-project-btn');
22+
expect(createBtn).toBeInTheDocument();
23+
expect(createBtn).toHaveAttribute('href', `/organizations/foo/projects/new/`);
24+
});
25+
26+
it('create project w/ disabled state including tooltip', async () => {
27+
render(<SetupReplaysCTA primaryAction="create" orgSlug="foo" disabled />);
28+
const createBtn = screen.getByTestId('create-project-btn');
29+
await userEvent.hover(createBtn);
30+
await waitFor(() => screen.getByTestId('create-project-tooltip'));
31+
expect(screen.getByTestId('create-project-tooltip')).toBeInTheDocument();
32+
});
33+
});

0 commit comments

Comments
 (0)