Browse Source

feat(project-creation): Make product selection feature available to everyone - Part 2 (#50806)

Priscila Oliveira 1 year ago
parent
commit
c73091159c

+ 10 - 7
static/app/components/onboarding/docWithProductSelection.tsx

@@ -4,24 +4,29 @@ import {motion} from 'framer-motion';
 import {Location} from 'history';
 
 import {Alert} from 'sentry/components/alert';
+import HookOrDefault from 'sentry/components/hookOrDefault';
 import LoadingError from 'sentry/components/loadingError';
 import LoadingIndicator from 'sentry/components/loadingIndicator';
 import {DocumentationWrapper} from 'sentry/components/onboarding/documentationWrapper';
 import {MissingExampleWarning} from 'sentry/components/onboarding/missingExampleWarning';
-import {PRODUCT, ProductSelection} from 'sentry/components/onboarding/productSelection';
+import {PRODUCT} from 'sentry/components/onboarding/productSelection';
 import {PlatformKey} from 'sentry/data/platformCategories';
 import platforms from 'sentry/data/platforms';
 import {t} from 'sentry/locale';
-import {Organization, Project} from 'sentry/types';
+import {Project} from 'sentry/types';
 import {OnboardingPlatformDoc} from 'sentry/types/onboarding';
 import {trackAnalytics} from 'sentry/utils/analytics';
 import getDynamicText from 'sentry/utils/getDynamicText';
 import {useApiQuery} from 'sentry/utils/queryClient';
+import useOrganization from 'sentry/utils/useOrganization';
 import SetupIntroduction from 'sentry/views/onboarding/components/setupIntroduction';
 import {SetupDocsLoader} from 'sentry/views/onboarding/setupDocsLoader';
 
+const ProductSelectionAvailabilityHook = HookOrDefault({
+  hookName: 'component:product-selection-availability',
+});
+
 export function DocWithProductSelection({
-  organization,
   location,
   newOrg,
   currentPlatform,
@@ -29,10 +34,10 @@ export function DocWithProductSelection({
 }: {
   currentPlatform: PlatformKey;
   location: Location;
-  organization: Organization;
   project: Project;
   newOrg?: boolean;
 }) {
+  const organization = useOrganization();
   const [showLoaderDocs, setShowLoaderDocs] = useState(currentPlatform === 'javascript');
 
   const loadPlatform = useMemo(() => {
@@ -91,9 +96,7 @@ export function DocWithProductSelection({
           platform={currentPlatform}
         />
       )}
-      <ProductSelection
-        defaultSelectedProducts={[PRODUCT.PERFORMANCE_MONITORING, PRODUCT.SESSION_REPLAY]}
-      />
+      <ProductSelectionAvailabilityHook organization={organization} />
       {isLoading ? (
         <LoadingIndicator />
       ) : isError ? (

+ 48 - 31
static/app/components/onboarding/productSelection.spec.tsx

@@ -1,11 +1,5 @@
 import {initializeOrg} from 'sentry-test/initializeOrg';
-import {
-  render,
-  screen,
-  userEvent,
-  waitFor,
-  within,
-} from 'sentry-test/reactTestingLibrary';
+import {render, screen, userEvent, waitFor} from 'sentry-test/reactTestingLibrary';
 import {textWithMarkupMatcher} from 'sentry-test/utils';
 
 import {PRODUCT, ProductSelection} from 'sentry/components/onboarding/productSelection';
@@ -34,41 +28,30 @@ describe('Onboarding Product Selection', function () {
     expect(screen.queryByText('Prefer to set up Sentry using')).not.toBeInTheDocument();
 
     // Error monitoring shall be checked and disabled by default
-    const errorMonitoring = screen.getByTestId(
-      `product-${PRODUCT.ERROR_MONITORING}-${PRODUCT.PERFORMANCE_MONITORING}-${PRODUCT.SESSION_REPLAY}`
-    );
-    expect(within(errorMonitoring).getByText('Error Monitoring')).toBeInTheDocument();
-    expect(within(errorMonitoring).getByRole('checkbox')).toBeChecked();
-    expect(within(errorMonitoring).getByRole('checkbox')).toBeDisabled();
+    expect(screen.getByRole('checkbox', {name: 'Error Monitoring'})).toBeChecked();
 
     // Tooltip with explanation shall be displayed on hover
-    await userEvent.hover(errorMonitoring);
+    await userEvent.hover(screen.getByRole('checkbox', {name: 'Error Monitoring'}));
     expect(
       await screen.findByText(/Let's admit it, we all have errors/)
     ).toBeInTheDocument();
 
     // Try to uncheck error monitoring
-    await userEvent.click(errorMonitoring);
+    await userEvent.click(screen.getByRole('checkbox', {name: 'Error Monitoring'}));
     await waitFor(() => expect(router.push).not.toHaveBeenCalled());
 
     // Performance monitoring shall be checked and enabled by default
-    const performanceMonitoring = screen.getByTestId(
-      `product-${PRODUCT.PERFORMANCE_MONITORING}`
-    );
-    expect(
-      within(performanceMonitoring).getByText('Performance Monitoring')
-    ).toBeInTheDocument();
-    expect(within(performanceMonitoring).getByRole('checkbox')).toBeChecked();
-    expect(within(performanceMonitoring).getByRole('checkbox')).toBeEnabled();
+    expect(screen.getByRole('checkbox', {name: 'Performance Monitoring'})).toBeChecked();
+    expect(screen.getByRole('checkbox', {name: 'Performance Monitoring'})).toBeEnabled();
 
     // Tooltip with explanation shall be displayed on hover
-    await userEvent.hover(performanceMonitoring);
+    await userEvent.hover(screen.getByRole('checkbox', {name: 'Performance Monitoring'}));
     expect(
       await screen.findByText(/Automatic performance issue detection/)
     ).toBeInTheDocument();
 
     // Uncheck performance monitoring
-    await userEvent.click(performanceMonitoring);
+    await userEvent.click(screen.getByRole('checkbox', {name: 'Performance Monitoring'}));
     await waitFor(() =>
       expect(router.replace).toHaveBeenCalledWith({
         pathname: undefined,
@@ -77,13 +60,11 @@ describe('Onboarding Product Selection', function () {
     );
 
     // Session replay shall be checked and enabled by default
-    const sessionReplay = screen.getByTestId(`product-${PRODUCT.SESSION_REPLAY}`);
-    expect(within(sessionReplay).getByText('Session Replay')).toBeInTheDocument();
-    expect(within(sessionReplay).getByRole('checkbox')).toBeChecked();
-    expect(within(sessionReplay).getByRole('checkbox')).toBeEnabled();
+    expect(screen.getByRole('checkbox', {name: 'Session Replay'})).toBeChecked();
+    expect(screen.getByRole('checkbox', {name: 'Session Replay'})).toBeEnabled();
 
     // Uncheck sesseion replay
-    await userEvent.click(sessionReplay);
+    await userEvent.click(screen.getByRole('checkbox', {name: 'Session Replay'}));
     await waitFor(() =>
       expect(router.replace).toHaveBeenCalledWith({
         pathname: undefined,
@@ -92,7 +73,7 @@ describe('Onboarding Product Selection', function () {
     );
 
     // Tooltip with explanation shall be displayed on hover
-    await userEvent.hover(sessionReplay);
+    await userEvent.hover(screen.getByRole('checkbox', {name: 'Session Replay'}));
     expect(
       await screen.findByText(/Video-like reproductions of user sessions/)
     ).toBeInTheDocument();
@@ -130,4 +111,40 @@ describe('Onboarding Product Selection', function () {
 
     expect(skipLazyLoader).toHaveBeenCalledTimes(1);
   });
+
+  it('renders disabled product', async function () {
+    const {router, routerContext} = initializeOrg({
+      router: {
+        location: {
+          query: {product: ['session-replay']},
+        },
+        params: {},
+      },
+    });
+
+    const disabledProducts = [
+      {
+        product: PRODUCT.PERFORMANCE_MONITORING,
+        reason: 'Product unavailable in this SDK version',
+      },
+    ];
+
+    render(<ProductSelection disabledProducts={disabledProducts} />, {
+      context: routerContext,
+    });
+
+    // Performance Monitoring shall be unchecked and disabled by default
+    expect(screen.getByRole('checkbox', {name: 'Performance Monitoring'})).toBeDisabled();
+    expect(
+      screen.getByRole('checkbox', {name: 'Performance Monitoring'})
+    ).not.toBeChecked();
+    await userEvent.hover(screen.getByRole('checkbox', {name: 'Performance Monitoring'}));
+
+    // A tooltip with explanation why the option is disabled shall be displayed on hover
+    expect(await screen.findByText(disabledProducts[0].reason)).toBeInTheDocument();
+    await userEvent.click(screen.getByRole('checkbox', {name: 'Performance Monitoring'}));
+
+    // Try to uncheck performance monitoring
+    await waitFor(() => expect(router.push).not.toHaveBeenCalled());
+  });
 });

+ 95 - 55
static/app/components/onboarding/productSelection.tsx

@@ -1,5 +1,4 @@
 import {Fragment, useCallback, useEffect} from 'react';
-import {css} from '@emotion/react';
 import styled from '@emotion/styled';
 
 import {Alert} from 'sentry/components/alert';
@@ -19,17 +18,51 @@ export enum PRODUCT {
   SESSION_REPLAY = 'session-replay',
 }
 
-type Props = {
+export type DisabledProduct = {
+  product: PRODUCT;
+  reason: string;
+};
+
+type ProductProps = {
+  checked: boolean;
+  disabled: boolean;
+  label: string;
+  onClick?: () => void;
+  permanentDisabled?: boolean;
+};
+
+function Product({disabled, permanentDisabled, checked, label, onClick}: ProductProps) {
+  return (
+    <ProductWrapper
+      permanentDisabled={permanentDisabled}
+      onClick={disabled ? undefined : onClick}
+      disabled={disabled}
+    >
+      <Checkbox
+        checked={checked}
+        disabled={permanentDisabled ? false : disabled}
+        aria-label={label}
+        size="xs"
+        readOnly
+      />
+      <span>{label}</span>
+    </ProductWrapper>
+  );
+}
+
+type ProductSelectionProps = {
   defaultSelectedProducts?: PRODUCT[];
+  disabledProducts?: DisabledProduct[];
   lazyLoader?: boolean;
   skipLazyLoader?: () => void;
 };
 
 export function ProductSelection({
   defaultSelectedProducts,
+  disabledProducts,
   lazyLoader,
   skipLazyLoader,
-}: Props) {
+}: ProductSelectionProps) {
   const router = useRouter();
   const products = decodeList(router.location.query.product);
 
@@ -62,6 +95,15 @@ export function ProductSelection({
     },
     [router, products]
   );
+
+  const performanceProductDisabled = disabledProducts?.find(
+    disabledProduct => disabledProduct.product === PRODUCT.PERFORMANCE_MONITORING
+  );
+
+  const sessionReplayProductDisabled = disabledProducts?.find(
+    disabledProduct => disabledProduct.product === PRODUCT.SESSION_REPLAY
+  );
+
   return (
     <Fragment>
       <TextBlock>
@@ -76,63 +118,51 @@ export function ProductSelection({
       </TextBlock>
       <Products>
         <Tooltip title={t("Let's admit it, we all have errors.")}>
-          <Product
-            disabled
-            data-test-id={`product-${PRODUCT.ERROR_MONITORING}-${PRODUCT.PERFORMANCE_MONITORING}-${PRODUCT.SESSION_REPLAY}`}
-          >
-            <Checkbox checked readOnly size="xs" disabled />
-            <div>{t('Error Monitoring')}</div>
-          </Product>
+          <Product disabled checked permanentDisabled label={t('Error Monitoring')} />
         </Tooltip>
         <Tooltip
           title={
-            <TooltipDescription>
-              {t(
-                'Automatic performance issue detection with context like who it impacts and the release, line of code, or function causing the slowdown.'
-              )}
-              <ExternalLink href="https://docs.sentry.io/platforms/javascript/guides/react/performance/">
-                {t('Read the Docs')}
-              </ExternalLink>
-            </TooltipDescription>
+            performanceProductDisabled?.reason ?? (
+              <TooltipDescription>
+                {t(
+                  'Automatic performance issue detection with context like who it impacts and the release, line of code, or function causing the slowdown.'
+                )}
+                <ExternalLink href="https://docs.sentry.io/platforms/javascript/guides/react/performance/">
+                  {t('Read the Docs')}
+                </ExternalLink>
+              </TooltipDescription>
+            )
           }
           isHoverable
         >
           <Product
             onClick={() => handleClickProduct(PRODUCT.PERFORMANCE_MONITORING)}
-            data-test-id={`product-${PRODUCT.PERFORMANCE_MONITORING}`}
-          >
-            <Checkbox
-              checked={products.includes(PRODUCT.PERFORMANCE_MONITORING)}
-              size="xs"
-              readOnly
-            />
-            {t('Performance Monitoring')}
-          </Product>
+            disabled={!!performanceProductDisabled}
+            checked={products.includes(PRODUCT.PERFORMANCE_MONITORING)}
+            label={t('Performance Monitoring')}
+          />
         </Tooltip>
         <Tooltip
           title={
-            <TooltipDescription>
-              {t(
-                'Video-like reproductions of user sessions with debugging context to help you confirm issue impact and troubleshoot faster.'
-              )}
-              <ExternalLink href="https://docs.sentry.io/platforms/javascript/guides/react/session-replay/">
-                {t('Read the Docs')}
-              </ExternalLink>
-            </TooltipDescription>
+            sessionReplayProductDisabled?.reason ?? (
+              <TooltipDescription>
+                {t(
+                  'Video-like reproductions of user sessions with debugging context to help you confirm issue impact and troubleshoot faster.'
+                )}
+                <ExternalLink href="https://docs.sentry.io/platforms/javascript/guides/react/session-replay/">
+                  {t('Read the Docs')}
+                </ExternalLink>
+              </TooltipDescription>
+            )
           }
           isHoverable
         >
           <Product
             onClick={() => handleClickProduct(PRODUCT.SESSION_REPLAY)}
-            data-test-id={`product-${PRODUCT.SESSION_REPLAY}`}
-          >
-            <Checkbox
-              checked={products.includes(PRODUCT.SESSION_REPLAY)}
-              size="xs"
-              readOnly
-            />
-            {t('Session Replay')}
-          </Product>
+            disabled={!!sessionReplayProductDisabled}
+            checked={products.includes(PRODUCT.SESSION_REPLAY)}
+            label={t('Session Replay')}
+          />
         </Tooltip>
       </Products>
       {lazyLoader && (
@@ -159,23 +189,33 @@ const Products = styled('div')`
   gap: ${space(1)};
 `;
 
-const Product = styled('div')<{disabled?: boolean}>`
+const ProductWrapper = styled('div')<{disabled?: boolean; permanentDisabled?: boolean}>`
   display: grid;
   grid-template-columns: repeat(3, max-content);
   gap: ${space(1)};
   align-items: center;
   ${p => p.theme.buttonPadding.xs};
-  background: ${p => p.theme.purple100};
-  border: 1px solid ${p => p.theme.purple300};
+  background: ${p =>
+    p.disabled && !p.permanentDisabled ? p.theme.background : p.theme.purple100};
+  border: 1px solid
+    ${p =>
+      p.disabled && !p.permanentDisabled ? p.theme.disabledBorder : p.theme.purple300};
   border-radius: 6px;
-  cursor: pointer;
-  ${p =>
-    p.disabled &&
-    css`
-      > *:not(:last-child) {
-        opacity: 0.5;
-      }
-    `};
+  cursor: ${p => (p.disabled ? 'not-allowed' : 'pointer')};
+  font-weight: 500;
+  color: ${p =>
+    p.disabled && !p.permanentDisabled ? p.theme.textColor : p.theme.purple300};
+  input {
+    cursor: ${p => (p.disabled ? 'not-allowed' : 'pointer')};
+  }
+
+  > *:first-child {
+    opacity: ${p => (p.permanentDisabled ? 0.5 : 1)};
+  }
+
+  > *:last-child {
+    opacity: ${p => (p.disabled ? 0.5 : 1)};
+  }
 `;
 
 const Divider = styled('hr')`

+ 5 - 0
static/app/types/hooks.tsx

@@ -110,6 +110,10 @@ type ProfilingAM1OrMMXUpgrade = {
   organization: Organization;
 };
 
+type ProductSelectionAvailabilityProps = {
+  organization: Organization;
+};
+
 type SetUpSdkDocProps = {
   location: Location;
   organization: Organization;
@@ -166,6 +170,7 @@ export type ComponentHooks = {
   'component:issue-priority-feedback': () => React.ComponentType<QualitativeIssueFeedbackProps>;
   'component:member-list-header': () => React.ComponentType<MemberListHeaderProps>;
   'component:org-stats-banner': () => React.ComponentType<DashboardHeadersProps>;
+  'component:product-selection-availability': () => React.ComponentType<ProductSelectionAvailabilityProps>;
   'component:profiling-am1-or-mmx-upgrade': () => React.ComponentType<ProfilingAM1OrMMXUpgrade>;
   'component:profiling-billing-banner': () => React.ComponentType<ProfilingBetaAlertBannerProps>;
   'component:profiling-upgrade-plan-button': () => React.ComponentType<ProfilingUpgradePlanButtonProps>;

+ 0 - 1
static/app/views/projectInstall/platform.tsx

@@ -354,7 +354,6 @@ export function ProjectInstallPlatform({location, params, route, router}: Props)
           />
         ) : showDocsWithProductSelection ? (
           <DocWithProductSelection
-            organization={organization}
             project={project}
             location={location}
             currentPlatform={platform.key}