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

ref(onboarding-docs): Convert java-spring-boot to new docs structure (#57771)

* Add types and forking logic for new structure
* Add test helpers for testing docs that follow the new structure
* Convert `java-spring-boot`
* Allow `React.ReactNode[]` for text content to enable using `tct()`
without wrapping it with a tag.

Closes https://github.com/getsentry/sentry/issues/57447
ArthurKnaus 1 год назад
Родитель
Сommit
6c8c3c2738

+ 17 - 15
static/app/components/onboarding/gettingStartedDoc/layout.tsx

@@ -7,6 +7,7 @@ import List from 'sentry/components/list';
 import ListItem from 'sentry/components/list/listItem';
 import {AuthTokenGeneratorProvider} from 'sentry/components/onboarding/gettingStartedDoc/authTokenGenerator';
 import {Step, StepProps} from 'sentry/components/onboarding/gettingStartedDoc/step';
+import {NextStep} from 'sentry/components/onboarding/gettingStartedDoc/types';
 import {PlatformOptionsControl} from 'sentry/components/onboarding/platformOptionsControl';
 import {ProductSelection} from 'sentry/components/onboarding/productSelection';
 import {t} from 'sentry/locale';
@@ -19,12 +20,6 @@ const ProductSelectionAvailabilityHook = HookOrDefault({
   defaultComponent: ProductSelection,
 });
 
-type NextStep = {
-  description: string;
-  link: string;
-  name: string;
-};
-
 export type LayoutProps = {
   projectSlug: string;
   steps: StepProps[];
@@ -52,14 +47,16 @@ export function Layout({
   return (
     <AuthTokenGeneratorProvider projectSlug={projectSlug}>
       <Wrapper>
-        {introduction && <Introduction>{introduction}</Introduction>}
-        <ProductSelectionAvailabilityHook
-          organization={organization}
-          platform={platformKey}
-        />
-        {platformOptions ? (
-          <PlatformOptionsControl platformOptions={platformOptions} />
-        ) : null}
+        <Header>
+          {introduction && <Introduction>{introduction}</Introduction>}
+          <ProductSelectionAvailabilityHook
+            organization={organization}
+            platform={platformKey}
+          />
+          {platformOptions ? (
+            <PlatformOptionsControl platformOptions={platformOptions} />
+          ) : null}
+        </Header>
         <Divider withBottomMargin={newOrg} />
         <Steps>
           {steps.map(step => (
@@ -86,6 +83,12 @@ export function Layout({
   );
 }
 
+const Header = styled('div')`
+  display: flex;
+  flex-direction: column;
+  gap: ${space(2)};
+`;
+
 const Divider = styled('hr')<{withBottomMargin?: boolean}>`
   height: 1px;
   width: 100%;
@@ -104,7 +107,6 @@ const Introduction = styled('div')`
   display: flex;
   flex-direction: column;
   gap: ${space(1)};
-  padding-bottom: ${space(2)};
 `;
 
 const Wrapper = styled('div')`

+ 174 - 0
static/app/components/onboarding/gettingStartedDoc/onboardingLayout.tsx

@@ -0,0 +1,174 @@
+import {Fragment, useMemo} from 'react';
+import styled from '@emotion/styled';
+
+import HookOrDefault from 'sentry/components/hookOrDefault';
+import ExternalLink from 'sentry/components/links/externalLink';
+import List from 'sentry/components/list';
+import ListItem from 'sentry/components/list/listItem';
+import {AuthTokenGeneratorProvider} from 'sentry/components/onboarding/gettingStartedDoc/authTokenGenerator';
+import {Step} from 'sentry/components/onboarding/gettingStartedDoc/step';
+import {Docs, DocsParams} from 'sentry/components/onboarding/gettingStartedDoc/types';
+import {useSourcePackageRegistries} from 'sentry/components/onboarding/gettingStartedDoc/useSourcePackageRegistries';
+import {
+  PlatformOptionsControl,
+  useUrlPlatformOptions,
+} from 'sentry/components/onboarding/platformOptionsControl';
+import {
+  ProductSelection,
+  ProductSolution,
+} from 'sentry/components/onboarding/productSelection';
+import {t} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import type {PlatformKey, Project} from 'sentry/types';
+import useOrganization from 'sentry/utils/useOrganization';
+
+const ProductSelectionAvailabilityHook = HookOrDefault({
+  hookName: 'component:product-selection-availability',
+  defaultComponent: ProductSelection,
+});
+
+export type OnboardingLayoutProps = {
+  docsConfig: Docs<any>;
+  dsn: string;
+  platformKey: PlatformKey;
+  projectId: Project['id'];
+  projectSlug: Project['slug'];
+  activeProductSelection?: ProductSolution[];
+  newOrg?: boolean;
+};
+
+const EMPTY_ARRAY: never[] = [];
+
+export function OnboardingLayout({
+  docsConfig,
+  dsn,
+  platformKey,
+  projectId,
+  projectSlug,
+  activeProductSelection = EMPTY_ARRAY,
+  newOrg,
+}: OnboardingLayoutProps) {
+  const organization = useOrganization();
+  const {isLoading: isLoadingRegistry, data: registryData} =
+    useSourcePackageRegistries(organization);
+  const selectedOptions = useUrlPlatformOptions(docsConfig.platformOptions);
+
+  const {platformOptions} = docsConfig;
+
+  const {introduction, steps, nextSteps} = useMemo(() => {
+    const {onboarding} = docsConfig;
+
+    const docParams: DocsParams<any> = {
+      dsn,
+      organization,
+      platformKey,
+      projectId,
+      projectSlug,
+      isPerformanceSelected: activeProductSelection.includes(
+        ProductSolution.PERFORMANCE_MONITORING
+      ),
+      isProfilingSelected: activeProductSelection.includes(ProductSolution.PROFILING),
+      isReplaySelected: activeProductSelection.includes(ProductSolution.SESSION_REPLAY),
+      sourcePackageRegistries: {
+        isLoading: isLoadingRegistry,
+        data: registryData,
+      },
+      platformOptions: selectedOptions,
+      newOrg,
+    };
+
+    return {
+      introduction: onboarding.introduction?.(docParams),
+      steps: [
+        ...onboarding.install(docParams),
+        ...onboarding.configure(docParams),
+        ...onboarding.verify(docParams),
+      ],
+      nextSteps: onboarding.nextSteps?.(docParams) || [],
+    };
+  }, [
+    activeProductSelection,
+    docsConfig,
+    dsn,
+    isLoadingRegistry,
+    newOrg,
+    organization,
+    platformKey,
+    projectId,
+    projectSlug,
+    registryData,
+    selectedOptions,
+  ]);
+
+  return (
+    <AuthTokenGeneratorProvider projectSlug={projectSlug}>
+      <Wrapper>
+        <Header>
+          {introduction && <div>{introduction}</div>}
+          <ProductSelectionAvailabilityHook
+            organization={organization}
+            platform={platformKey}
+          />
+          {platformOptions ? (
+            <PlatformOptionsControl platformOptions={platformOptions} />
+          ) : null}
+        </Header>
+        <Divider withBottomMargin />
+        <Steps>
+          {steps.map(step => (
+            <Step key={step.title ?? step.type} {...step} />
+          ))}
+        </Steps>
+        {nextSteps.length > 0 && (
+          <Fragment>
+            <Divider />
+            <h4>{t('Next Steps')}</h4>
+            <List symbol="bullet">
+              {nextSteps.map(step => (
+                <ListItem key={step.name}>
+                  <ExternalLink href={step.link}>{step.name}</ExternalLink>
+                  {': '}
+                  {step.description}
+                </ListItem>
+              ))}
+            </List>
+          </Fragment>
+        )}
+      </Wrapper>
+    </AuthTokenGeneratorProvider>
+  );
+}
+
+const Header = styled('div')`
+  display: flex;
+  flex-direction: column;
+  gap: ${space(2)};
+`;
+
+const Divider = styled('hr')<{withBottomMargin?: boolean}>`
+  height: 1px;
+  width: 100%;
+  background: ${p => p.theme.border};
+  border: none;
+  ${p => p.withBottomMargin && `margin-bottom: ${space(3)}`}
+`;
+
+const Steps = styled('div')`
+  display: flex;
+  flex-direction: column;
+  gap: 1.5rem;
+`;
+
+const Wrapper = styled('div')`
+  h4 {
+    margin-bottom: 0.5em;
+  }
+  && {
+    p {
+      margin-bottom: 0;
+    }
+    h5 {
+      margin-bottom: 0;
+    }
+  }
+`;

+ 31 - 9
static/app/components/onboarding/gettingStartedDoc/sdkDocumentation.tsx

@@ -1,6 +1,8 @@
 import {useEffect, useState} from 'react';
 
 import LoadingIndicator from 'sentry/components/loadingIndicator';
+import {OnboardingLayout} from 'sentry/components/onboarding/gettingStartedDoc/onboardingLayout';
+import {Docs} from 'sentry/components/onboarding/gettingStartedDoc/types';
 import {useSourcePackageRegistries} from 'sentry/components/onboarding/gettingStartedDoc/useSourcePackageRegistries';
 import {ProductSolution} from 'sentry/components/onboarding/productSelection';
 import type {
@@ -15,10 +17,10 @@ import {useApiQuery} from 'sentry/utils/queryClient';
 type SdkDocumentationProps = {
   activeProductSelection: ProductSolution[];
   organization: Organization;
-  platform: PlatformIntegration | null;
+  platform: PlatformIntegration;
+  projectId: Project['id'];
   projectSlug: Project['slug'];
   newOrg?: boolean;
-  projectId?: Project['id'];
 };
 
 export type ModuleProps = {
@@ -32,6 +34,11 @@ export type ModuleProps = {
   sourcePackageRegistries?: ReturnType<typeof useSourcePackageRegistries>;
 };
 
+function isFunctionalComponent(obj: any): obj is React.ComponentType<ModuleProps> {
+  // As we only use function components in the docs this should suffice
+  return typeof obj === 'function';
+}
+
 // Loads the component containing the documentation for the specified platform
 export function SdkDocumentation({
   platform,
@@ -44,7 +51,7 @@ export function SdkDocumentation({
   const sourcePackageRegistries = useSourcePackageRegistries(organization);
 
   const [module, setModule] = useState<null | {
-    default: React.ComponentType<ModuleProps>;
+    default: Docs<any> | React.ComponentType<ModuleProps>;
   }>(null);
 
   // TODO: This will be removed once we no longer rely on sentry-docs to load platform icons
@@ -89,7 +96,7 @@ export function SdkDocumentation({
     if (projectKeysIsError || projectKeysIsLoading) {
       return;
     }
-
+    setModule(null);
     async function getGettingStartedDoc() {
       const mod = await import(
         /* webpackExclude: /.spec/ */
@@ -104,18 +111,33 @@ export function SdkDocumentation({
     return <LoadingIndicator />;
   }
 
-  const {default: GettingStartedDoc} = module;
+  const {default: docs} = module;
+
+  if (isFunctionalComponent(docs)) {
+    const GettingStartedDoc = docs;
+    return (
+      <GettingStartedDoc
+        dsn={projectKeys[0].dsn.public}
+        activeProductSelection={activeProductSelection}
+        newOrg={newOrg}
+        platformKey={platform.id}
+        organization={organization}
+        projectId={projectId}
+        projectSlug={projectSlug}
+        sourcePackageRegistries={sourcePackageRegistries}
+      />
+    );
+  }
 
   return (
-    <GettingStartedDoc
+    <OnboardingLayout
+      docsConfig={docs}
       dsn={projectKeys[0].dsn.public}
       activeProductSelection={activeProductSelection}
       newOrg={newOrg}
-      platformKey={platform?.id}
-      organization={organization}
+      platformKey={platform.id}
       projectId={projectId}
       projectSlug={projectSlug}
-      sourcePackageRegistries={sourcePackageRegistries}
     />
   );
 }

+ 3 - 2
static/app/components/onboarding/gettingStartedDoc/step.tsx

@@ -112,6 +112,7 @@ type ConfigurationType = {
   partialLoading?: boolean;
 };
 
+// TODO(aknaus): move to types
 interface BaseStepProps {
   /**
    * Additional information to be displayed below the configurations
@@ -121,7 +122,7 @@ interface BaseStepProps {
   /**
    * A brief description of the step
    */
-  description?: React.ReactNode;
+  description?: React.ReactNode | React.ReactNode[];
 }
 interface StepPropsWithTitle extends BaseStepProps {
   title: string;
@@ -227,7 +228,7 @@ const Configurations = styled(Configuration)`
   margin-top: ${space(2)};
 `;
 
-const Description = styled(Configuration)`
+const Description = styled('div')`
   code {
     color: ${p => p.theme.pink400};
   }

+ 74 - 0
static/app/components/onboarding/gettingStartedDoc/types.ts

@@ -0,0 +1,74 @@
+import type {StepProps} from 'sentry/components/onboarding/gettingStartedDoc/step';
+import type {ReleaseRegistrySdk} from 'sentry/components/onboarding/gettingStartedDoc/useSourcePackageRegistries';
+import type {Organization, PlatformKey, Project} from 'sentry/types';
+
+type GeneratorFunction<T, Params> = (params: Params) => T;
+type WithGeneratorProperties<T extends Record<string, any>, Params> = {
+  [key in keyof T]: GeneratorFunction<T[key], Params>;
+};
+
+export interface PlatformOption<Value extends string = string> {
+  /**
+   * Array of items for the option. Each one representing a selectable value.
+   */
+  items: {
+    label: string;
+    value: Value;
+  }[];
+  /**
+   * The name of the option
+   */
+  label: string;
+  /**
+   * The default value to be used on initial render
+   */
+  defaultValue?: string;
+}
+
+export type BasePlatformOptions = Record<string, PlatformOption<string>>;
+
+export type SelectedPlatformOptions<
+  PlatformOptions extends BasePlatformOptions = BasePlatformOptions,
+> = {
+  [key in keyof PlatformOptions]: PlatformOptions[key]['items'][number]['value'];
+};
+
+export interface DocsParams<
+  PlatformOptions extends BasePlatformOptions = BasePlatformOptions,
+> {
+  dsn: string;
+  isPerformanceSelected: boolean;
+  isProfilingSelected: boolean;
+  isReplaySelected: boolean;
+  organization: Organization;
+  platformKey: PlatformKey;
+  platformOptions: SelectedPlatformOptions<PlatformOptions>;
+  projectId: Project['id'];
+  projectSlug: Project['slug'];
+  sourcePackageRegistries: {isLoading: boolean; data?: ReleaseRegistrySdk};
+  newOrg?: boolean;
+}
+
+export interface NextStep {
+  description: string;
+  link: string;
+  name: string;
+}
+
+export interface OnboardingConfig<
+  PlatformOptions extends BasePlatformOptions = BasePlatformOptions,
+> extends WithGeneratorProperties<
+    {
+      configure: StepProps[];
+      install: StepProps[];
+      verify: StepProps[];
+      introduction?: React.ReactNode | React.ReactNode[];
+      nextSteps?: NextStep[];
+    },
+    DocsParams<PlatformOptions>
+  > {}
+
+export interface Docs<PlatformOptions extends BasePlatformOptions = BasePlatformOptions> {
+  onboarding: OnboardingConfig<PlatformOptions>;
+  platformOptions?: PlatformOptions;
+}

+ 1 - 1
static/app/components/onboarding/gettingStartedDoc/useSourcePackageRegistries.tsx

@@ -4,7 +4,7 @@ import {Organization} from 'sentry/types';
 import {handleXhrErrorResponse} from 'sentry/utils/handleXhrErrorResponse';
 import {useApiQuery} from 'sentry/utils/queryClient';
 
-type ReleaseRegistrySdk = Record<
+export type ReleaseRegistrySdk = Record<
   string,
   {
     canonical: string;

+ 2 - 4
static/app/components/onboarding/platformOptionsControl.spec.tsx

@@ -1,10 +1,8 @@
 import {initializeOrg} from 'sentry-test/initializeOrg';
 import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
 
-import {
-  PlatformOption,
-  PlatformOptionsControl,
-} from 'sentry/components/onboarding/platformOptionsControl';
+import {PlatformOption} from 'sentry/components/onboarding/gettingStartedDoc/types';
+import {PlatformOptionsControl} from 'sentry/components/onboarding/platformOptionsControl';
 
 describe('Onboarding Product Selection', function () {
   const platformOptions: Record<string, PlatformOption> = {

+ 23 - 36
static/app/components/onboarding/platformOptionsControl.tsx

@@ -1,58 +1,46 @@
 import {useMemo} from 'react';
 import styled from '@emotion/styled';
 
+import {
+  BasePlatformOptions,
+  PlatformOption,
+  SelectedPlatformOptions,
+} from 'sentry/components/onboarding/gettingStartedDoc/types';
 import {SegmentedControl} from 'sentry/components/segmentedControl';
 import {space} from 'sentry/styles/space';
 import useRouter from 'sentry/utils/useRouter';
 
-export interface PlatformOption<K extends string = string> {
-  /**
-   * Array of items for the option. Each one representing a selectable value.
-   */
-  items: {
-    label: string;
-    value: K;
-  }[];
-  /**
-   * The name of the option
-   */
-  label: string;
-  /**
-   * The default value to be used on initial render
-   */
-  defaultValue?: string;
-}
-
 /**
  * Hook that returns the currently selected platform option values from the URL
  * it will fallback to the defaultValue or the first option value if the value in the URL is not valid or not present
  */
-export function useUrlPlatformOptions<K extends string>(
-  platformOptions: Record<K, PlatformOption>
-): Record<K, string> {
+export function useUrlPlatformOptions<PlatformOptions extends BasePlatformOptions>(
+  platformOptions?: PlatformOptions
+): SelectedPlatformOptions<PlatformOptions> {
   const router = useRouter();
   const {query} = router.location;
 
-  return useMemo(
-    () =>
-      Object.keys(platformOptions).reduce(
-        (acc, key) => {
-          const defaultValue = platformOptions[key as K].defaultValue;
-          const values = platformOptions[key as K].items.map(({value}) => value);
-          acc[key] = values.includes(query[key]) ? query[key] : defaultValue ?? values[0];
-          return acc;
-        },
-        {} as Record<K, string>
-      ),
-    [platformOptions, query]
-  );
+  return useMemo(() => {
+    if (!platformOptions) {
+      return {} as SelectedPlatformOptions<PlatformOptions>;
+    }
+
+    return Object.keys(platformOptions).reduce((acc, key) => {
+      const defaultValue = platformOptions[key].defaultValue;
+      const values = platformOptions[key].items.map(({value}) => value);
+      acc[key as keyof PlatformOptions] = values.includes(query[key])
+        ? query[key]
+        : defaultValue ?? values[0];
+      return acc;
+    }, {} as SelectedPlatformOptions<PlatformOptions>);
+  }, [platformOptions, query]);
 }
 
 type OptionControlProps = {
   /**
    * The platform options for which the control is rendered
    */
-  option: PlatformOption;
+  option: PlatformOption<any>;
   /**
    * Value of the currently selected item
    */
@@ -113,7 +101,6 @@ export function PlatformOptionsControl({platformOptions}: PlatformOptionsControl
 }
 
 const Options = styled('div')`
-  padding-top: ${space(2)};
   display: flex;
   flex-wrap: wrap;
   gap: ${space(1)};

+ 3 - 2
static/app/components/onboarding/productSelection.tsx

@@ -18,6 +18,7 @@ import {decodeList} from 'sentry/utils/queryString';
 import useRouter from 'sentry/utils/useRouter';
 import TextBlock from 'sentry/views/settings/components/text/textBlock';
 
+// TODO(aknaus): move to types
 export enum ProductSolution {
   ERROR_MONITORING = 'error-monitoring',
   PERFORMANCE_MONITORING = 'performance-monitoring',
@@ -340,7 +341,7 @@ export function ProductSelection({
   return (
     <Fragment>
       {showPackageManagerInfo && (
-        <TextBlock>
+        <TextBlock noMargin>
           {lazyLoader
             ? tct('In this quick guide you’ll use our [loaderScript] to set up:', {
                 loaderScript: <strong>Loader Script</strong>,
@@ -473,5 +474,5 @@ const TooltipDescription = styled('div')`
 `;
 
 const AlternativeInstallationAlert = styled(Alert)`
-  margin-top: ${space(3)};
+  margin-bottom: 0px;
 `;

+ 2 - 4
static/app/gettingStartedDocs/android/android.tsx

@@ -6,10 +6,8 @@ import ListItem from 'sentry/components/list/listItem';
 import {Layout, LayoutProps} from 'sentry/components/onboarding/gettingStartedDoc/layout';
 import {ModuleProps} from 'sentry/components/onboarding/gettingStartedDoc/sdkDocumentation';
 import {StepType} from 'sentry/components/onboarding/gettingStartedDoc/step';
-import {
-  PlatformOption,
-  useUrlPlatformOptions,
-} from 'sentry/components/onboarding/platformOptionsControl';
+import {PlatformOption} from 'sentry/components/onboarding/gettingStartedDoc/types';
+import {useUrlPlatformOptions} from 'sentry/components/onboarding/platformOptionsControl';
 import {ProductSolution} from 'sentry/components/onboarding/productSelection';
 import {t, tct} from 'sentry/locale';
 

Некоторые файлы не были показаны из-за большого количества измененных файлов