import {Fragment} from 'react'; import {ClassNames} from '@emotion/react'; import styled from '@emotion/styled'; import {IconBusiness} from 'sentry/icons'; import {space} from 'sentry/styles/space'; import type {Organization} from 'sentry/types/organization'; import withOrganization from 'sentry/utils/withOrganization'; import PowerFeatureHovercard from 'getsentry/components/powerFeatureHovercard'; import withSubscription from 'getsentry/components/withSubscription'; import {useBillingConfig} from 'getsentry/hooks/useBillingConfig'; import type {Subscription} from 'getsentry/types'; interface ChildRenderProps { Wrapper: React.FunctionComponent<{children: React.ReactElement}>; additionalContent: React.ReactElement | null; disabled: boolean; } interface Props { children: (opts: ChildRenderProps) => React.ReactElement; id: string; organization: Organization; subscription: Subscription; } /** @internal exported for tests only */ export function SidebarNavigationItem({id, organization, subscription, children}: Props) { const {data: billingConfig} = useBillingConfig({organization, subscription}); const subscriptionPlan = subscription.planDetails; const subscriptionPlanFeatures = subscriptionPlan?.features ?? []; const trialPlan = subscription.trialPlan ? billingConfig?.planList?.find(plan => plan.id === subscription.trialPlan) : undefined; const trialPlanFeatures = trialPlan?.features ?? []; const planFeatures = [...new Set([...subscriptionPlanFeatures, ...trialPlanFeatures])]; const rule = NavigationItemAccessRule.forId(id, organization, planFeatures); return children(rule.props); } interface CheckableForAccess { get props(): ChildRenderProps; } class NavigationItemAccessRule implements CheckableForAccess { id: string; organization: Organization; planFeatures: string[]; constructor(id: string, organization: Organization, planFeatures: string[]) { this.id = id; this.organization = organization; this.planFeatures = planFeatures; } get props(): ChildRenderProps { return { disabled: false, additionalContent: null, Wrapper: Fragment, }; } static forId( id: string, organization: Organization, planFeatures: string[] ): CheckableForAccess { let cls: any; if (id === 'sidebar-accordion-insights-item') { cls = InsightsAccordionAccessRule; } else if (Object.keys(INSIGHTS_LINK_ID_FEATURE_REQUIREMENTS).includes(id)) { cls = InsightsItemAccessRule; } else { cls = NavigationItemAccessRule; } return new cls(id, organization, planFeatures); } } export class InsightsItemAccessRule extends NavigationItemAccessRule { get doesOrganizationHaveAnyInsightsAccess() { return ( this.organization?.features?.includes('insights-initial-modules') || this.organization?.features?.includes('insights-addon-modules') ); } get hasRequiredFeatures(): boolean { // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message const requiredFeatures = INSIGHTS_LINK_ID_FEATURE_REQUIREMENTS[this.id] ?? []; const enabledFeatures = [...this.planFeatures, ...this.organization.features]; return requiredFeatures.every((feature: any) => enabledFeatures.includes(feature)); } get props() { // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message const requiredFeatures = INSIGHTS_LINK_ID_FEATURE_REQUIREMENTS[this.id] ?? []; const hasRequiredFeatures = this.hasRequiredFeatures; // Show the Turbo link if the organization doesn't have access to that link, but hide it if they don't have access to _any_ Insight modules, since in that case there'd be a Turbo icon next to each item const hasTurboIcon = !hasRequiredFeatures && this.doesOrganizationHaveAnyInsightsAccess; return { disabled: !hasRequiredFeatures, additionalContent: hasTurboIcon ? : null, Wrapper: hasRequiredFeatures ? Fragment : makeUpsellWrapper(this.id, requiredFeatures ?? []), }; } } class InsightsAccordionAccessRule extends InsightsItemAccessRule { get props() { // If the organization has access to _some_ modules, leave the Insights link alone. If it doesn't have any access to Insights modules, show an upsell around the "Insights" link. return this.doesOrganizationHaveAnyInsightsAccess ? { disabled: false, additionalContent: null, Wrapper: Fragment, } : { disabled: true, additionalContent: , Wrapper: makeUpsellWrapper(this.id, ['insights-initial-modules']), }; } } const CenteredIcon = styled(IconBusiness)` display: inline-flex; flex-shrink: 0; margin-left: ${space(1)}; `; function makeUpsellWrapper( id: string, requiredFeatures: string[] ): React.FunctionComponent<{children: React.ReactElement}> { // @ts-expect-error TS(7031): Binding element 'upsellWrapperChildren' implicitly... Remove this comment to see the full error message function UpsellWrapper({children: upsellWrapperChildren}) { return ( {({css}) => ( {upsellWrapperChildren} )} ); } return UpsellWrapper; } // Each key is an `id` prop of `SidebarItem` components. Each value is a list of plan feature strings that id needs const INSIGHTS_LINK_ID_FEATURE_REQUIREMENTS = { 'performance-database': ['insights-initial-modules'], 'performance-http': ['insights-initial-modules'], 'performance-webvitals': ['insights-initial-modules'], 'performance-mobile-screens': ['insights-initial-modules'], 'performance-mobile-app-startup': ['insights-initial-modules'], 'performance-browser-resources': ['insights-initial-modules'], 'performance-cache': ['insights-addon-modules'], 'performance-queues': ['insights-addon-modules'], 'performance-mobile-ui': ['insights-addon-modules'], 'llm-monitoring': ['insights-addon-modules'], 'performance-screen-rendering': ['insights-addon-modules'], }; export type InsightSidebarId = keyof typeof INSIGHTS_LINK_ID_FEATURE_REQUIREMENTS; export default withOrganization( withSubscription(SidebarNavigationItem, {noLoader: true}) );