|
@@ -1,47 +1,62 @@
|
|
|
-import {useEffect, useMemo, useState} from 'react';
|
|
|
+import {Fragment, useEffect, useMemo, useState} from 'react';
|
|
|
import styled from '@emotion/styled';
|
|
|
+import partition from 'lodash/partition';
|
|
|
|
|
|
-import {Button} from 'sentry/components/button';
|
|
|
import {CompactSelect} from 'sentry/components/compactSelect';
|
|
|
import IdBadge from 'sentry/components/idBadge';
|
|
|
+import LoadingError from 'sentry/components/loadingError';
|
|
|
import LoadingIndicator from 'sentry/components/loadingIndicator';
|
|
|
-import useOnboardingDocs from 'sentry/components/onboardingWizard/useOnboardingDocs';
|
|
|
-import {
|
|
|
- DocumentationWrapper,
|
|
|
- OnboardingStep,
|
|
|
-} from 'sentry/components/sidebar/onboardingStep';
|
|
|
-import {
|
|
|
- EventIndicator,
|
|
|
- TaskSidebar,
|
|
|
- TaskSidebarList,
|
|
|
-} from 'sentry/components/sidebar/taskSidebar';
|
|
|
+import {Step} from 'sentry/components/onboarding/gettingStartedDoc/step';
|
|
|
+import type {DocsParams} from 'sentry/components/onboarding/gettingStartedDoc/types';
|
|
|
+import {useLoadGettingStarted} from 'sentry/components/onboarding/gettingStartedDoc/utils/useLoadGettingStarted';
|
|
|
+import {ProductSolution} from 'sentry/components/onboarding/productSelection';
|
|
|
+import {TaskSidebar} from 'sentry/components/sidebar/taskSidebar';
|
|
|
import type {CommonSidebarProps} from 'sentry/components/sidebar/types';
|
|
|
import {SidebarPanelKey} from 'sentry/components/sidebar/types';
|
|
|
import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
|
|
|
import platforms from 'sentry/data/platforms';
|
|
|
-import {t, tct} from 'sentry/locale';
|
|
|
+import {t} from 'sentry/locale';
|
|
|
import {space} from 'sentry/styles/space';
|
|
|
-import type {Project, SelectValue} from 'sentry/types';
|
|
|
+import type {SelectValue} from 'sentry/types/core';
|
|
|
+import type {Organization} from 'sentry/types/organization';
|
|
|
+import type {PlatformIntegration, Project} from 'sentry/types/project';
|
|
|
import {trackAnalytics} from 'sentry/utils/analytics';
|
|
|
-import EventWaiter from 'sentry/utils/eventWaiter';
|
|
|
-import useApi from 'sentry/utils/useApi';
|
|
|
+import {getDocsPlatformSDKForPlatform} from 'sentry/utils/profiling/platforms';
|
|
|
import useOrganization from 'sentry/utils/useOrganization';
|
|
|
import usePageFilters from 'sentry/utils/usePageFilters';
|
|
|
-import usePrevious from 'sentry/utils/usePrevious';
|
|
|
import useProjects from 'sentry/utils/useProjects';
|
|
|
|
|
|
-import {makeDocKeyMap, splitProjectsByProfilingSupport} from './util';
|
|
|
+function splitProjectsByProfilingSupport(projects: Project[]): {
|
|
|
+ supported: Project[];
|
|
|
+ unsupported: Project[];
|
|
|
+} {
|
|
|
+ const [supported, unsupported] = partition(
|
|
|
+ projects,
|
|
|
+ project => project.platform && getDocsPlatformSDKForPlatform(project.platform)
|
|
|
+ );
|
|
|
+
|
|
|
+ return {supported, unsupported};
|
|
|
+}
|
|
|
+
|
|
|
+const PROFILING_ONBOARDING_STEPS = [
|
|
|
+ ProductSolution.PERFORMANCE_MONITORING,
|
|
|
+ ProductSolution.PROFILING,
|
|
|
+];
|
|
|
|
|
|
export function ProfilingOnboardingSidebar(props: CommonSidebarProps) {
|
|
|
- const {currentPanel, collapsed, hidePanel, orientation} = props;
|
|
|
- const isActive = currentPanel === SidebarPanelKey.PROFILING_ONBOARDING;
|
|
|
- const organization = useOrganization();
|
|
|
- const hasProjectAccess = organization.access.includes('project:read');
|
|
|
+ if (props.currentPanel !== SidebarPanelKey.PROFILING_ONBOARDING) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ return <ProfilingOnboarding {...props} />;
|
|
|
+}
|
|
|
|
|
|
+function ProfilingOnboarding(props: CommonSidebarProps) {
|
|
|
+ const pageFilters = usePageFilters();
|
|
|
+ const organization = useOrganization();
|
|
|
const {projects} = useProjects();
|
|
|
|
|
|
const [currentProject, setCurrentProject] = useState<Project | undefined>();
|
|
|
- const pageFilters = usePageFilters();
|
|
|
|
|
|
const {supported: supportedProjects, unsupported: unsupportedProjects} = useMemo(
|
|
|
() => splitProjectsByProfilingSupport(projects),
|
|
@@ -49,6 +64,8 @@ export function ProfilingOnboardingSidebar(props: CommonSidebarProps) {
|
|
|
);
|
|
|
|
|
|
useEffect(() => {
|
|
|
+ if (currentProject) return;
|
|
|
+
|
|
|
// we'll only ever select an unsupportedProject if they do not have a supported project in their organization
|
|
|
if (supportedProjects.length === 0 && unsupportedProjects.length > 0) {
|
|
|
if (pageFilters.selection.projects[0] === ALL_ACCESS_PROJECTS) {
|
|
@@ -87,8 +104,8 @@ export function ProfilingOnboardingSidebar(props: CommonSidebarProps) {
|
|
|
}
|
|
|
}
|
|
|
}, [
|
|
|
- pageFilters.selection.projects,
|
|
|
currentProject,
|
|
|
+ pageFilters.selection.projects,
|
|
|
supportedProjects,
|
|
|
unsupportedProjects,
|
|
|
]);
|
|
@@ -130,23 +147,23 @@ export function ProfilingOnboardingSidebar(props: CommonSidebarProps) {
|
|
|
];
|
|
|
}, [supportedProjects, unsupportedProjects]);
|
|
|
|
|
|
- if (!isActive || !hasProjectAccess) {
|
|
|
- return null;
|
|
|
- }
|
|
|
+ const currentPlatform = currentProject?.platform
|
|
|
+ ? platforms.find(p => p.id === currentProject.platform)
|
|
|
+ : undefined;
|
|
|
|
|
|
return (
|
|
|
<TaskSidebar
|
|
|
- orientation={orientation}
|
|
|
- collapsed={collapsed}
|
|
|
+ orientation={props.orientation}
|
|
|
+ collapsed={props.collapsed}
|
|
|
hidePanel={() => {
|
|
|
trackAnalytics('profiling_views.onboarding_action', {
|
|
|
organization,
|
|
|
action: 'dismissed',
|
|
|
});
|
|
|
- hidePanel();
|
|
|
+ props.hidePanel();
|
|
|
}}
|
|
|
>
|
|
|
- <TaskSidebarList>
|
|
|
+ <Content>
|
|
|
<Heading>{t('Profile Code')}</Heading>
|
|
|
<div
|
|
|
onClick={e => {
|
|
@@ -177,178 +194,125 @@ export function ProfilingOnboardingSidebar(props: CommonSidebarProps) {
|
|
|
position="bottom-end"
|
|
|
/>
|
|
|
</div>
|
|
|
- {currentProject && (
|
|
|
- <OnboardingContent
|
|
|
- currentProject={currentProject}
|
|
|
- isSupported={supportedProjects.includes(currentProject)}
|
|
|
+ {currentProject && currentPlatform ? (
|
|
|
+ <ProfilingOnboardingContent
|
|
|
+ activeProductSelection={PROFILING_ONBOARDING_STEPS}
|
|
|
+ organization={organization}
|
|
|
+ platform={currentPlatform}
|
|
|
+ projectId={currentProject.id}
|
|
|
+ projectSlug={currentProject.slug}
|
|
|
/>
|
|
|
- )}
|
|
|
- </TaskSidebarList>
|
|
|
+ ) : null}
|
|
|
+ </Content>
|
|
|
</TaskSidebar>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
-function OnboardingContent({
|
|
|
- currentProject,
|
|
|
- isSupported,
|
|
|
-}: {
|
|
|
- currentProject: Project;
|
|
|
- isSupported: boolean;
|
|
|
-}) {
|
|
|
- const currentPlatform = platforms.find(p => p.id === currentProject?.platform);
|
|
|
- const api = useApi();
|
|
|
- const organization = useOrganization();
|
|
|
- const [received, setReceived] = useState(false);
|
|
|
- const previousProject = usePrevious(currentProject);
|
|
|
- useEffect(() => {
|
|
|
- if (!currentProject || !previousProject) {
|
|
|
- return;
|
|
|
- }
|
|
|
- if (previousProject.id !== currentProject.id) {
|
|
|
- setReceived(false);
|
|
|
- }
|
|
|
- }, [currentProject, previousProject]);
|
|
|
-
|
|
|
- const docKeysMap = useMemo(() => makeDocKeyMap(currentPlatform?.id), [currentPlatform]);
|
|
|
- const docKeys = useMemo(
|
|
|
- () => (docKeysMap ? Object.values(docKeysMap) : []),
|
|
|
- [docKeysMap]
|
|
|
- );
|
|
|
+interface ProfilingOnboardingContentProps {
|
|
|
+ activeProductSelection: ProductSolution[];
|
|
|
+ organization: Organization;
|
|
|
+ platform: PlatformIntegration;
|
|
|
+ projectId: Project['id'];
|
|
|
+ projectSlug: Project['slug'];
|
|
|
+}
|
|
|
|
|
|
- const {docContents, isLoading, hasOnboardingContents} = useOnboardingDocs({
|
|
|
- docKeys,
|
|
|
- project: currentProject,
|
|
|
- isPlatformSupported: isSupported,
|
|
|
+function ProfilingOnboardingContent(props: ProfilingOnboardingContentProps) {
|
|
|
+ const {isLoading, isError, dsn, cdn, docs, refetch} = useLoadGettingStarted({
|
|
|
+ orgSlug: props.organization.slug,
|
|
|
+ projSlug: props.projectSlug,
|
|
|
+ platform: props.platform,
|
|
|
});
|
|
|
|
|
|
if (isLoading) {
|
|
|
return <LoadingIndicator />;
|
|
|
}
|
|
|
|
|
|
- if (!currentPlatform) {
|
|
|
+ if (isError) {
|
|
|
return (
|
|
|
- <ContentContainer>
|
|
|
- <p>
|
|
|
- {t(
|
|
|
- `Your project's platform has not been set. Please select your project's platform before proceeding.`
|
|
|
- )}
|
|
|
- </p>
|
|
|
- <Button
|
|
|
- size="sm"
|
|
|
- to={`/settings/${organization.slug}/projects/${currentProject.slug}/`}
|
|
|
- >
|
|
|
- {t('Go to Project Settings')}
|
|
|
- </Button>
|
|
|
- </ContentContainer>
|
|
|
+ <LoadingError
|
|
|
+ message={t(
|
|
|
+ 'We encountered an issue while loading the getting started documentation for this platform.'
|
|
|
+ )}
|
|
|
+ />
|
|
|
);
|
|
|
}
|
|
|
|
|
|
- if (!isSupported) {
|
|
|
- // this content will only be presented if the org only has one project and its not supported
|
|
|
- // in these scenarios we will auto-select the unsupported project and render this message
|
|
|
+ if (!docs) {
|
|
|
return (
|
|
|
- <ContentContainer>
|
|
|
- <p>
|
|
|
- {tct(
|
|
|
- 'Fiddlesticks. Profiling isn’t available for your [platform] project yet. Reach out to us on Discord for more information.',
|
|
|
- {platform: currentPlatform?.name || currentProject.slug}
|
|
|
- )}
|
|
|
- </p>
|
|
|
- <Button size="sm" href="https://discord.gg/zrMjKA4Vnz" external>
|
|
|
- {t('Join Discord')}
|
|
|
- </Button>
|
|
|
- </ContentContainer>
|
|
|
+ <LoadingError
|
|
|
+ message={t(
|
|
|
+ 'The getting started documentation for this platform is currently unavailable.'
|
|
|
+ )}
|
|
|
+ />
|
|
|
);
|
|
|
}
|
|
|
|
|
|
- if (!docKeysMap || !hasOnboardingContents) {
|
|
|
+ if (!dsn) {
|
|
|
return (
|
|
|
- <ContentContainer>
|
|
|
- <p>
|
|
|
- {tct(
|
|
|
- 'Fiddlesticks. This checklist isn’t available for your [project] project yet, but for now, go to Sentry docs for installation details.',
|
|
|
- {project: currentProject.slug}
|
|
|
- )}
|
|
|
- </p>
|
|
|
- <Button
|
|
|
- size="sm"
|
|
|
- href="https://docs.sentry.io/product/profiling/getting-started/"
|
|
|
- external
|
|
|
- >
|
|
|
- {t('Go to documentation')}
|
|
|
- </Button>
|
|
|
- </ContentContainer>
|
|
|
+ <LoadingError
|
|
|
+ message={t(
|
|
|
+ 'We encountered an issue while loading the DSN for this getting started documentation.'
|
|
|
+ )}
|
|
|
+ onRetry={refetch}
|
|
|
+ />
|
|
|
);
|
|
|
}
|
|
|
|
|
|
- const alertContent = docContents[docKeysMap['0-alert']];
|
|
|
+ const docParams: DocsParams<any> = {
|
|
|
+ cdn,
|
|
|
+ dsn,
|
|
|
+ organization: props.organization,
|
|
|
+ platformKey: props.platform.id,
|
|
|
+ projectId: props.projectId,
|
|
|
+ projectSlug: props.projectSlug,
|
|
|
+ isFeedbackSelected: false,
|
|
|
+ isPerformanceSelected: true,
|
|
|
+ isProfilingSelected: true,
|
|
|
+ isReplaySelected: false,
|
|
|
+ sourcePackageRegistries: {
|
|
|
+ isLoading: false,
|
|
|
+ data: undefined,
|
|
|
+ },
|
|
|
+ platformOptions: PROFILING_ONBOARDING_STEPS,
|
|
|
+ newOrg: false,
|
|
|
+ feedbackOptions: {},
|
|
|
+ };
|
|
|
+
|
|
|
+ const steps = [
|
|
|
+ ...docs.onboarding.install(docParams),
|
|
|
+ ...docs.onboarding.configure(docParams),
|
|
|
+ ];
|
|
|
|
|
|
return (
|
|
|
- <ContentContainer>
|
|
|
- {alertContent && (
|
|
|
- <DocumentationWrapper dangerouslySetInnerHTML={{__html: alertContent}} />
|
|
|
+ <Fragment>
|
|
|
+ {docs.onboarding.introduction && (
|
|
|
+ <Introduction>{docs.onboarding.introduction(docParams)}</Introduction>
|
|
|
)}
|
|
|
- <p>
|
|
|
- {t(
|
|
|
- `Adding Profiling to your %s project is simple. Make sure you've got these basics down.`,
|
|
|
- currentPlatform!.name
|
|
|
- )}
|
|
|
- </p>
|
|
|
- {Object.entries(docKeysMap).map(entry => {
|
|
|
- const [key, docKey] = entry;
|
|
|
- if (key === '0-alert') {
|
|
|
- return null;
|
|
|
- }
|
|
|
-
|
|
|
- const content = docContents[docKey];
|
|
|
- if (!content) {
|
|
|
- return null;
|
|
|
- }
|
|
|
- return (
|
|
|
- <div key={docKey}>
|
|
|
- <OnboardingStep
|
|
|
- prefix="profiling"
|
|
|
- docKey={docKey}
|
|
|
- project={currentProject}
|
|
|
- docContent={content}
|
|
|
- />
|
|
|
- </div>
|
|
|
- );
|
|
|
- })}
|
|
|
- <EventWaiter
|
|
|
- api={api}
|
|
|
- organization={organization}
|
|
|
- project={currentProject}
|
|
|
- eventType="profile"
|
|
|
- onIssueReceived={() => {
|
|
|
- trackAnalytics('profiling_views.onboarding_action', {
|
|
|
- organization,
|
|
|
- action: 'done',
|
|
|
- });
|
|
|
- setReceived(true);
|
|
|
- }}
|
|
|
- >
|
|
|
- {() => (received ? <EventReceivedIndicator /> : <EventWaitingIndicator />)}
|
|
|
- </EventWaiter>
|
|
|
- </ContentContainer>
|
|
|
+ <Steps>
|
|
|
+ {steps.map(step => {
|
|
|
+ return <Step key={step.title ?? step.type} {...step} />;
|
|
|
+ })}
|
|
|
+ </Steps>
|
|
|
+ </Fragment>
|
|
|
);
|
|
|
}
|
|
|
|
|
|
-function EventReceivedIndicator() {
|
|
|
- return (
|
|
|
- <EventIndicator status="received">
|
|
|
- {t("We've received this project's first profile!")}
|
|
|
- </EventIndicator>
|
|
|
- );
|
|
|
-}
|
|
|
+const Steps = styled('div')`
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 1.5rem;
|
|
|
+`;
|
|
|
|
|
|
-function EventWaitingIndicator() {
|
|
|
- return (
|
|
|
- <EventIndicator status="waiting">
|
|
|
- {t("Waiting for this project's first profile.")}
|
|
|
- </EventIndicator>
|
|
|
- );
|
|
|
-}
|
|
|
+const Introduction = styled('div')`
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ margin-top: ${space(2)};
|
|
|
+ margin-bottom: ${space(2)};
|
|
|
+`;
|
|
|
+
|
|
|
+const Content = styled('div')`
|
|
|
+ padding: ${space(2)};
|
|
|
+`;
|
|
|
|
|
|
const Heading = styled('div')`
|
|
|
display: flex;
|
|
@@ -365,7 +329,3 @@ const StyledIdBadge = styled(IdBadge)`
|
|
|
white-space: nowrap;
|
|
|
flex-shrink: 1;
|
|
|
`;
|
|
|
-
|
|
|
-const ContentContainer = styled('div')`
|
|
|
- margin: ${space(2)} 0;
|
|
|
-`;
|