Browse Source

ref(onboarding): Add variants to the react docs - (#46037)

Priscila Oliveira 2 years ago
parent
commit
242fd1dd06

+ 3 - 2
build-utils/integration-docs-fetch-plugin.ts

@@ -35,8 +35,8 @@ type PlatformsData = {
   platforms: Record<string, PlatformItem>;
 };
 
-const transformPlatformsToList = ({platforms}: PlatformsData) =>
-  Object.keys(platforms)
+const transformPlatformsToList = ({platforms}: PlatformsData) => {
+  return Object.keys(platforms)
     .map(platformId => {
       const integrationMap = platforms[platformId];
       const integrations = Object.keys(integrationMap)
@@ -55,6 +55,7 @@ const transformPlatformsToList = ({platforms}: PlatformsData) =>
       };
     })
     .sort(alphaSortFromKey(item => item.name));
+};
 
 class IntegrationDocsFetchPlugin {
   modulePath: string;

+ 11 - 6
static/app/actionCreators/projects.tsx

@@ -346,12 +346,17 @@ export function removeProject(
  * @param projectSlug Project Slug
  * @param platform Project platform.
  */
-export function loadDocs(
-  api: Client,
-  orgSlug: string,
-  projectSlug: string,
-  platform: PlatformKey
-) {
+export function loadDocs({
+  api,
+  orgSlug,
+  projectSlug,
+  platform,
+}: {
+  api: Client;
+  orgSlug: string;
+  platform: PlatformKey;
+  projectSlug: string;
+}) {
   return api.requestPromise(`/projects/${orgSlug}/${projectSlug}/docs/${platform}/`);
 }
 

+ 6 - 1
static/app/components/events/interfaces/spans/inlineDocs.tsx

@@ -65,7 +65,12 @@ class InlineDocs extends Component<Props, State> {
     }
 
     try {
-      const {html, link} = await loadDocs(api, orgSlug, projectSlug, tracingPlatform);
+      const {html, link} = await loadDocs({
+        api,
+        orgSlug,
+        projectSlug,
+        platform: tracingPlatform,
+      });
       this.setState({html, link});
     } catch (error) {
       Sentry.captureException(error);

+ 84 - 0
static/app/components/onboarding/productSelection.spec.tsx

@@ -0,0 +1,84 @@
+import {initializeOrg} from 'sentry-test/initializeOrg';
+import {
+  render,
+  screen,
+  userEvent,
+  waitFor,
+  within,
+} from 'sentry-test/reactTestingLibrary';
+import {textWithMarkupMatcher} from 'sentry-test/utils';
+
+import {PRODUCT, ProductSelection} from 'sentry/components/onboarding/productSelection';
+
+describe('Onboarding Product Selection', function () {
+  it('renders default state', async function () {
+    const {router, routerContext} = initializeOrg({
+      ...initializeOrg(),
+      router: {
+        location: {
+          query: {product: ['performance-monitoring', 'session-replay']},
+        },
+        params: {},
+      },
+    });
+
+    render(<ProductSelection />, {
+      context: routerContext,
+    });
+
+    // Introduction
+    expect(
+      screen.getByText(textWithMarkupMatcher(/In this quick guide you’ll use/))
+    ).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();
+
+    // Try to uncheck error monitoring
+    await userEvent.click(errorMonitoring);
+    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();
+
+    // Uncheck performance monitoring
+    await userEvent.click(performanceMonitoring);
+    await waitFor(() =>
+      expect(router.push).toHaveBeenCalledWith({
+        pathname: undefined,
+        query: {product: ['session-replay']},
+      })
+    );
+
+    // 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();
+
+    // Uncheck sesseion replay
+    await userEvent.click(sessionReplay);
+    await waitFor(() =>
+      expect(router.push).toHaveBeenCalledWith({
+        pathname: undefined,
+        query: {product: ['performance-monitoring']},
+      })
+    );
+
+    // Tooltip with explanation shall be displayed on hover
+    await userEvent.hover(within(sessionReplay).getByTestId('more-information'));
+    expect(await screen.findByRole('link', {name: 'Read the Docs'})).toBeInTheDocument();
+  });
+});

+ 162 - 0
static/app/components/onboarding/productSelection.tsx

@@ -0,0 +1,162 @@
+import {Fragment, useCallback, useMemo} from 'react';
+import {css} from '@emotion/react';
+import styled from '@emotion/styled';
+
+import Checkbox from 'sentry/components/checkbox';
+import ExternalLink from 'sentry/components/links/externalLink';
+import QuestionTooltip from 'sentry/components/questionTooltip';
+import {t, tct} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import useRouter from 'sentry/utils/useRouter';
+import TextBlock from 'sentry/views/settings/components/text/textBlock';
+
+export enum PRODUCT {
+  ERROR_MONITORING = 'error-monitoring',
+  PERFORMANCE_MONITORING = 'performance-monitoring',
+  SESSION_REPLAY = 'session-replay',
+}
+
+export function ProductSelection() {
+  const router = useRouter();
+  const products = useMemo(() => {
+    return Array.isArray(router.location.query.product)
+      ? router.location.query.product
+      : [router.location.query.product];
+  }, [router.location.query.product]);
+
+  const handleClickProduct = useCallback(
+    (product: PRODUCT) => {
+      router.push({
+        pathname: router.location.pathname,
+        query: {
+          ...router.location.query,
+          product: products.includes(product)
+            ? products.filter(p => p !== product)
+            : [...products, product],
+        },
+      });
+    },
+    [router, products]
+  );
+  return (
+    <Fragment>
+      <TextBlock>
+        {tct('In this quick guide you’ll use [npm] or [yarn] to set up:', {
+          npm: <strong>npm</strong>,
+          yarn: <strong>yarn</strong>,
+        })}
+      </TextBlock>
+      <Products>
+        <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>
+          <QuestionTooltip
+            size="xs"
+            title={
+              <TooltipDescription>
+                {t(
+                  'Detailed views of errors and performance problems in your application grouped by events with a similar set of characteristics.'
+                )}
+                <ExternalLink href="https://docs.sentry.io/platforms/javascript/guides/react/">
+                  {t('Read the Docs')}
+                </ExternalLink>
+              </TooltipDescription>
+            }
+            isHoverable
+          />
+        </Product>
+        <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')}
+          <QuestionTooltip
+            size="xs"
+            title={
+              <TooltipDescription>
+                {t(
+                  'Detailed views of errors and performance problems in your application grouped by events with a similar set of characteristics.'
+                )}
+                <ExternalLink href="https://docs.sentry.io/platforms/javascript/guides/react/">
+                  {t('Read the Docs')}
+                </ExternalLink>
+              </TooltipDescription>
+            }
+            isHoverable
+          />
+        </Product>
+        <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')}
+          <QuestionTooltip
+            size="xs"
+            title={
+              <TooltipDescription>
+                {t(
+                  'Detailed views of errors and performance problems in your application grouped by events with a similar set of characteristics.'
+                )}
+                <ExternalLink href="https://docs.sentry.io/platforms/javascript/guides/react/">
+                  {t('Read the Docs')}
+                </ExternalLink>
+              </TooltipDescription>
+            }
+            isHoverable
+          />
+        </Product>
+      </Products>
+      <Divider />
+    </Fragment>
+  );
+}
+
+const Products = styled('div')`
+  display: flex;
+  flex-wrap: wrap;
+  gap: ${space(1)};
+`;
+
+const Product = styled('div')<{disabled?: 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};
+  border-radius: 6px;
+  cursor: pointer;
+  ${p =>
+    p.disabled &&
+    css`
+      > *:not(:last-child) {
+        opacity: 0.5;
+      }
+    `};
+`;
+
+const Divider = styled('hr')`
+  border-top-color: ${p => p.theme.border};
+`;
+
+const TooltipDescription = styled('div')`
+  display: flex;
+  flex-direction: column;
+  gap: ${space(0.5)};
+  justify-content: flex-start;
+  text-align: left;
+`;

+ 6 - 1
static/app/components/onboardingWizard/useOnboardingDocs.tsx

@@ -73,7 +73,12 @@ function useOnboardingDocs({docKeys, isPlatformSupported, project}: Options) {
 
       setLoadingDoc(true);
 
-      loadDocs(api, organization.slug, project.slug, docKey as any)
+      loadDocs({
+        api,
+        orgSlug: organization.slug,
+        projectSlug: project.slug,
+        platform: docKey as any,
+      })
         .then(({html}) => {
           setDocContent(html as string);
           setLoadingDoc(false);

+ 1 - 0
static/app/components/platformPicker.tsx

@@ -60,6 +60,7 @@ class PlatformPicker extends Component<PlatformPickerProps, State> {
 
   get platformList() {
     const {category} = this.state;
+
     const currentCategory = categoryList.find(({id}) => id === category);
 
     const filter = this.state.filter.toLowerCase();

+ 38 - 7
static/app/data/platforms.tsx

@@ -19,24 +19,55 @@ const otherPlatform = {
   name: t('Other'),
 };
 
+export enum ReactDocVariant {
+  ErrorMonitoring = 'javascript-react-with-error-monitoring',
+  ErrorMonitoringAndPerformance = 'javascript-react-with-error-monitoring-and-performance',
+  ErrorMonitoringAndSessionReplay = 'javascript-react-with-error-monitoring-and-replay',
+  ErrorMonitoringPerformanceAndReplay = 'javascript-react-with-error-monitoring-performance-and-replay',
+}
+
 const platformIntegrations: PlatformIntegration[] = [
   ...integrationDocsPlatforms.platforms,
   otherPlatform,
 ]
   .map(platform => {
-    const integrations = platform.integrations
-      .map(i => ({...i, language: platform.id} as PlatformIntegration))
+    const integrations = platform.integrations.reduce((acc, value) => {
+      // filter out any javascript-react-* platforms; as they're not meant to be used as a platform in the PlatformPicker component
+      // but only to load specific documentation for the React SDK
+      if (Object.values(ReactDocVariant).includes(value.id as ReactDocVariant)) {
+        return acc;
+      }
+
       // filter out any tracing platforms; as they're not meant to be used as a platform for
       // the project creation flow
-      .filter(integration => !(tracing as readonly string[]).includes(integration.id))
+      if ((tracing as readonly string[]).includes(value.id)) {
+        return acc;
+      }
+
       // filter out any performance onboarding documentation
-      .filter(integration => !integration.id.includes('performance-onboarding'))
+      if (value.id.includes('performance-onboarding')) {
+        return acc;
+      }
+
       // filter out any replay onboarding documentation
-      .filter(integration => !integration.id.includes('replay-onboarding'))
+      if (value.id.includes('replay-onboarding')) {
+        return acc;
+      }
+
       // filter out any profiling onboarding documentation
-      .filter(integration => !integration.id.includes('profiling-onboarding'));
+      if (value.id.includes('profiling-onboarding')) {
+        return acc;
+      }
+
+      if (!acc[value.id]) {
+        acc[value.id] = {...value, language: platform.id};
+        return acc;
+      }
+
+      return acc;
+    }, {});
 
-    return integrations;
+    return Object.values(integrations) as PlatformIntegration[];
   })
   .flat();
 

+ 7 - 8
static/app/views/onboarding/components/createProjectsFooter.tsx

@@ -31,7 +31,7 @@ import GenericFooter from './genericFooter';
 type Props = {
   clearPlatforms: () => void;
   genSkipOnboardingLink: () => React.ReactNode;
-  onComplete: () => void;
+  onComplete: (selectedPlatforms: PlatformKey[]) => void;
   organization: Organization;
   platforms: PlatformKey[];
 };
@@ -49,11 +49,10 @@ export default function CreateProjectsFooter({
 
   const api = useApi();
   const {teams} = useTeams();
-  const [persistedOnboardingState, setPersistedOnboardingState] =
-    usePersistedOnboardingState();
+  const [clientState, setClientState] = usePersistedOnboardingState();
 
   const createProjects = async () => {
-    if (!persistedOnboardingState) {
+    if (!clientState) {
       // Do nothing if client state is not loaded yet.
       return;
     }
@@ -65,7 +64,7 @@ export default function CreateProjectsFooter({
 
       const responses = await Promise.all(
         platforms
-          .filter(platform => !persistedOnboardingState.platformToProjectIdMap[platform])
+          .filter(platform => !clientState.platformToProjectIdMap[platform])
           .map(platform =>
             createProject(api, organization.slug, teams[0].slug, platform, platform, {
               defaultRules: true,
@@ -73,13 +72,13 @@ export default function CreateProjectsFooter({
           )
       );
       const nextState: OnboardingState = {
-        platformToProjectIdMap: persistedOnboardingState.platformToProjectIdMap,
+        platformToProjectIdMap: clientState.platformToProjectIdMap,
         selectedPlatforms: platforms,
         state: 'projects_selected',
         url: 'setup-docs/',
       };
       responses.forEach(p => (nextState.platformToProjectIdMap[p.platform] = p.slug));
-      setPersistedOnboardingState(nextState);
+      setClientState(nextState);
 
       responses.forEach(data => ProjectsStore.onCreateSuccess(data, organization.slug));
 
@@ -89,7 +88,7 @@ export default function CreateProjectsFooter({
         organization,
       });
       clearIndicators();
-      setTimeout(onComplete);
+      setTimeout(() => onComplete(platforms));
     } catch (err) {
       addErrorMessage(
         singleSelectPlatform

+ 0 - 21
static/app/views/onboarding/components/fullIntroduction.tsx

@@ -1,21 +0,0 @@
-import {PlatformKey} from 'sentry/data/platformCategories';
-import platforms from 'sentry/data/platforms';
-import {t} from 'sentry/locale';
-
-import SetupIntroduction from './setupIntroduction';
-
-type Props = {
-  currentPlatform: PlatformKey;
-};
-
-export default function FullIntroduction({currentPlatform}: Props) {
-  return (
-    <SetupIntroduction
-      stepHeaderText={t(
-        'Prepare the %s SDK',
-        platforms.find(p => p.id === currentPlatform)?.name ?? ''
-      )}
-      platform={currentPlatform}
-    />
-  );
-}

Some files were not shown because too many files changed in this diff