Просмотр исходного кода

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:
https://github.com/getsentry/sentry/issues/45474#event-8866165824
Elias Hussary 1 год назад
Родитель
Сommit
89c6efbdc3

+ 139 - 17
static/app/views/replays/list/replayOnboardingPanel.tsx

@@ -4,15 +4,22 @@ import styled from '@emotion/styled';
 import emptyStateImg from 'sentry-images/spot/replays-empty-state.svg';
 import emptyStateImg from 'sentry-images/spot/replays-empty-state.svg';
 
 
 import Feature from 'sentry/components/acl/feature';
 import Feature from 'sentry/components/acl/feature';
+import Alert from 'sentry/components/alert';
 import {Button} from 'sentry/components/button';
 import {Button} from 'sentry/components/button';
 import ButtonBar from 'sentry/components/buttonBar';
 import ButtonBar from 'sentry/components/buttonBar';
 import HookOrDefault from 'sentry/components/hookOrDefault';
 import HookOrDefault from 'sentry/components/hookOrDefault';
+import ExternalLink from 'sentry/components/links/externalLink';
 import OnboardingPanel from 'sentry/components/onboardingPanel';
 import OnboardingPanel from 'sentry/components/onboardingPanel';
-import {t} from 'sentry/locale';
+import {Tooltip} from 'sentry/components/tooltip';
+import {replayPlatforms} from 'sentry/data/platformCategories';
+import {IconInfo} from 'sentry/icons';
+import {t, tct} from 'sentry/locale';
 import PreferencesStore from 'sentry/stores/preferencesStore';
 import PreferencesStore from 'sentry/stores/preferencesStore';
 import {useLegacyStore} from 'sentry/stores/useLegacyStore';
 import {useLegacyStore} from 'sentry/stores/useLegacyStore';
 import {useReplayOnboardingSidebarPanel} from 'sentry/utils/replays/hooks/useReplayOnboarding';
 import {useReplayOnboardingSidebarPanel} from 'sentry/utils/replays/hooks/useReplayOnboarding';
 import useOrganization from 'sentry/utils/useOrganization';
 import useOrganization from 'sentry/utils/useOrganization';
+import usePageFilters from 'sentry/utils/usePageFilters';
+import useProjects from 'sentry/utils/useProjects';
 
 
 type Breakpoints = {
 type Breakpoints = {
   large: string;
   large: string;
@@ -28,6 +35,34 @@ const OnboardingCTAHook = HookOrDefault({
 
 
 export default function ReplayOnboardingPanel() {
 export default function ReplayOnboardingPanel() {
   const preferences = useLegacyStore(PreferencesStore);
   const preferences = useLegacyStore(PreferencesStore);
+  const pageFilters = usePageFilters();
+  const projects = useProjects();
+  const organization = useOrganization();
+  const canCreateProjects = organization.access.includes('project:admin');
+
+  const selectedProjects = projects.projects.filter(p =>
+    pageFilters.selection.projects.includes(Number(p.id))
+  );
+
+  const hasSelectedProjects = selectedProjects.length > 0;
+
+  const allProjectsUnsupported = projects.projects.every(
+    p => !replayPlatforms.includes(p.platform!)
+  );
+
+  const allSelectedProjectsUnsupported = selectedProjects.every(
+    p => !replayPlatforms.includes(p.platform!)
+  );
+
+  // if all projects are unsupported we should prompt the user to create a project
+  // else we prompt to setup
+  const primaryAction = allProjectsUnsupported ? 'create' : 'setup';
+  // disable "create" if the user has insufficient permissions
+  // disable "setup" if the current selected pageFilters are not supported
+  const primaryActionDisabled =
+    primaryAction === 'create'
+      ? !canCreateProjects
+      : allSelectedProjectsUnsupported && hasSelectedProjects;
 
 
   const breakpoints = preferences.collapsed
   const breakpoints = preferences.collapsed
     ? {
     ? {
@@ -43,26 +78,115 @@ export default function ReplayOnboardingPanel() {
         xlarge: '1450px',
         xlarge: '1450px',
       };
       };
 
 
-  const organization = useOrganization();
-
   return (
   return (
-    <OnboardingPanel image={<HeroImage src={emptyStateImg} breakpoints={breakpoints} />}>
-      <Feature
-        features={['session-replay-ga']}
-        organization={organization}
-        renderDisabled={() => <SetupReplaysCTA />}
+    <Fragment>
+      {hasSelectedProjects && allSelectedProjectsUnsupported && (
+        <Alert icon={<IconInfo />}>
+          {tct(
+            `[projectMsg] [action] a project using our [link], or equivalent framework SDK.`,
+            {
+              action: primaryAction === 'create' ? t('Create') : t('Select'),
+              projectMsg: (
+                <strong>
+                  {t(
+                    `Session Replay isn't available for project %s.`,
+                    selectedProjects[0].slug
+                  )}
+                </strong>
+              ),
+              link: (
+                <ExternalLink href="https://docs.sentry.io/platforms/javascript/session-replay/">
+                  {t('Sentry browser SDK package')}
+                </ExternalLink>
+              ),
+            }
+          )}
+        </Alert>
+      )}
+      <OnboardingPanel
+        image={<HeroImage src={emptyStateImg} breakpoints={breakpoints} />}
       >
       >
-        <OnboardingCTAHook organization={organization}>
-          <SetupReplaysCTA />
-        </OnboardingCTAHook>
-      </Feature>
-    </OnboardingPanel>
+        <Feature
+          features={['session-replay-ga']}
+          organization={organization}
+          renderDisabled={() => (
+            <SetupReplaysCTA
+              orgSlug={organization.slug}
+              primaryAction={primaryAction}
+              disabled={primaryActionDisabled}
+            />
+          )}
+        >
+          <OnboardingCTAHook organization={organization}>
+            <SetupReplaysCTA
+              orgSlug={organization.slug}
+              primaryAction={primaryAction}
+              disabled={primaryActionDisabled}
+            />
+          </OnboardingCTAHook>
+        </Feature>
+      </OnboardingPanel>
+    </Fragment>
   );
   );
 }
 }
 
 
-function SetupReplaysCTA() {
+interface SetupReplaysCTAProps {
+  orgSlug: string;
+  primaryAction: 'setup' | 'create';
+  disabled?: boolean;
+}
+
+export function SetupReplaysCTA({
+  disabled,
+  primaryAction = 'setup',
+  orgSlug,
+}: SetupReplaysCTAProps) {
   const {activateSidebar} = useReplayOnboardingSidebarPanel();
   const {activateSidebar} = useReplayOnboardingSidebarPanel();
 
 
+  function renderCTA() {
+    if (primaryAction === 'setup') {
+      return (
+        <Tooltip
+          title={
+            <span data-test-id="setup-replays-tooltip">
+              {t('Select a supported project from the projects dropdown.')}
+            </span>
+          }
+          disabled={!disabled} // we only want to show the tooltip when the button is disabled
+        >
+          <Button
+            data-test-id="setup-replays-btn"
+            onClick={activateSidebar}
+            priority="primary"
+            disabled={disabled}
+          >
+            {t('Set Up Replays')}
+          </Button>
+        </Tooltip>
+      );
+    }
+
+    return (
+      <Tooltip
+        title={
+          <span data-test-id="create-project-tooltip">
+            {t('Only admins, managers, and owners, can create projects.')}
+          </span>
+        }
+        disabled={!disabled}
+      >
+        <Button
+          data-test-id="create-project-btn"
+          to={`/organizations/${orgSlug}/projects/new/`}
+          priority="primary"
+          disabled={disabled}
+        >
+          {t('Create Project')}
+        </Button>
+      </Tooltip>
+    );
+  }
+
   return (
   return (
     <Fragment>
     <Fragment>
       <h3>{t('Get to the root cause faster')}</h3>
       <h3>{t('Get to the root cause faster')}</h3>
@@ -72,9 +196,7 @@ function SetupReplaysCTA() {
         )}
         )}
       </p>
       </p>
       <ButtonList gap={1}>
       <ButtonList gap={1}>
-        <Button onClick={activateSidebar} priority="primary">
-          {t('Set Up Replays')}
-        </Button>
+        {renderCTA()}
         <Button
         <Button
           href="https://docs.sentry.io/platforms/javascript/session-replay/"
           href="https://docs.sentry.io/platforms/javascript/session-replay/"
           external
           external

+ 33 - 0
static/app/views/replays/list/setupReplaysCTA.spec.tsx

@@ -0,0 +1,33 @@
+import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
+
+import {SetupReplaysCTA} from 'sentry/views/replays/list/replayOnboardingPanel';
+
+describe('SetupReplaysCTA', () => {
+  it('renders setup replay', () => {
+    render(<SetupReplaysCTA primaryAction="setup" orgSlug="foo" />);
+    expect(screen.getByTestId('setup-replays-btn')).toBeInTheDocument();
+  });
+
+  it('renders setup replay w/ disabled state including tooltip', async () => {
+    render(<SetupReplaysCTA primaryAction="setup" orgSlug="foo" disabled />);
+    const setupBtn = screen.getByTestId('setup-replays-btn');
+    await userEvent.hover(setupBtn);
+    await waitFor(() => screen.getByTestId('setup-replays-tooltip'));
+    expect(screen.getByTestId('setup-replays-tooltip')).toBeInTheDocument();
+  });
+
+  it('create project', () => {
+    render(<SetupReplaysCTA primaryAction="create" orgSlug="foo" />);
+    const createBtn = screen.getByTestId('create-project-btn');
+    expect(createBtn).toBeInTheDocument();
+    expect(createBtn).toHaveAttribute('href', `/organizations/foo/projects/new/`);
+  });
+
+  it('create project w/ disabled state including tooltip', async () => {
+    render(<SetupReplaysCTA primaryAction="create" orgSlug="foo" disabled />);
+    const createBtn = screen.getByTestId('create-project-btn');
+    await userEvent.hover(createBtn);
+    await waitFor(() => screen.getByTestId('create-project-tooltip'));
+    expect(screen.getByTestId('create-project-tooltip')).toBeInTheDocument();
+  });
+});