diff --git a/frontend/__snapshots__/components-networkrequesttiming--basic.png b/frontend/__snapshots__/components-networkrequesttiming--basic.png index ff34c2d27d479..8247b433f71c8 100644 Binary files a/frontend/__snapshots__/components-networkrequesttiming--basic.png and b/frontend/__snapshots__/components-networkrequesttiming--basic.png differ diff --git a/frontend/src/layout/navigation/TopBar/TopBar.tsx b/frontend/src/layout/navigation/TopBar/TopBar.tsx index 585cb5554b1c0..9c307caa1c6f7 100644 --- a/frontend/src/layout/navigation/TopBar/TopBar.tsx +++ b/frontend/src/layout/navigation/TopBar/TopBar.tsx @@ -16,12 +16,19 @@ import { featureFlagLogic } from 'lib/logic/featureFlagLogic' import { FEATURE_FLAGS } from 'lib/constants' import { NotebookButton } from '~/layout/navigation/TopBar/NotebookButton' import { ActivationSidebarToggle } from 'lib/components/ActivationSidebar/ActivationSidebarToggle' +import { organizationLogic } from 'scenes/organizationLogic' +import { LemonButtonWithDropdown, Lettermark } from '@posthog/lemon-ui' +import { ProjectSwitcherOverlay } from '../ProjectSwitcher' +import { topBarLogic } from './topBarLogic' export function TopBar(): JSX.Element { const { isSideBarShown, noSidebar, minimalTopBar, mobileLayout } = useValues(navigationLogic) const { toggleSideBarBase, toggleSideBarMobile } = useActions(navigationLogic) const { groupNamesTaxonomicTypes } = useValues(groupsModel) const { featureFlags } = useValues(featureFlagLogic) + const { currentOrganization } = useValues(organizationLogic) + const { isProjectSwitcherShown } = useValues(topBarLogic) + const { toggleProjectSwitcher, hideProjectSwitcher } = useActions(topBarLogic) const hasNotebooks = !!featureFlags[FEATURE_FLAGS.NOTEBOOKS] @@ -71,11 +78,32 @@ export function TopBar(): JSX.Element { )}
- {!minimalTopBar && ( + {!minimalTopBar ? ( <> {hasNotebooks && } + ) : ( + currentOrganization?.teams && + currentOrganization.teams.length > 1 && ( +
+ } + onClick={() => toggleProjectSwitcher()} + dropdown={{ + visible: isProjectSwitcherShown, + onClickOutside: hideProjectSwitcher, + overlay: , + actionable: true, + placement: 'top-end', + }} + type="secondary" + fullWidth + > + Switch project + +
+ ) )} diff --git a/frontend/src/layout/navigation/TopBar/topBarLogic.ts b/frontend/src/layout/navigation/TopBar/topBarLogic.ts new file mode 100644 index 0000000000000..3ef8f08138acd --- /dev/null +++ b/frontend/src/layout/navigation/TopBar/topBarLogic.ts @@ -0,0 +1,20 @@ +import { actions, kea, reducers, path } from 'kea' + +import type { topBarLogicType } from './topBarLogicType' + +export const topBarLogic = kea([ + path(['layout', 'navigation', 'TopBar', 'topBarLogic']), + actions({ + toggleProjectSwitcher: true, + hideProjectSwitcher: true, + }), + reducers({ + isProjectSwitcherShown: [ + false, + { + toggleProjectSwitcher: (state) => !state, + hideProjectSwitcher: () => false, + }, + ], + }), +]) diff --git a/frontend/src/scenes/billing/BillingHero.scss b/frontend/src/scenes/billing/BillingHero.scss index 0928d8d935c0c..3c30a1ff02d79 100644 --- a/frontend/src/scenes/billing/BillingHero.scss +++ b/frontend/src/scenes/billing/BillingHero.scss @@ -18,5 +18,5 @@ .BillingHero__hog__img { height: 200px; width: 200px; - margin: -20px -30px; + margin: -20px 0; } diff --git a/frontend/src/scenes/billing/BillingHero.tsx b/frontend/src/scenes/billing/BillingHero.tsx index 0f74dc168071a..3dbe62ca6dc62 100644 --- a/frontend/src/scenes/billing/BillingHero.tsx +++ b/frontend/src/scenes/billing/BillingHero.tsx @@ -1,9 +1,12 @@ import { BlushingHog } from 'lib/components/hedgehogs' import './BillingHero.scss' +import useResizeObserver from 'use-resize-observer' export const BillingHero = (): JSX.Element => { + const { width, ref: billingHeroRef } = useResizeObserver() + return ( -
+

How pricing works

Get the whole hog.

@@ -13,9 +16,11 @@ export const BillingHero = (): JSX.Element => { limits as low as $0 to control spend.

-
- -
+ {width && width > 500 && ( +
+ +
+ )}
) } diff --git a/frontend/src/scenes/billing/PlanComparison.scss b/frontend/src/scenes/billing/PlanComparison.scss index 57c4ed6bd6d63..98d091b0b80d7 100644 --- a/frontend/src/scenes/billing/PlanComparison.scss +++ b/frontend/src/scenes/billing/PlanComparison.scss @@ -29,6 +29,10 @@ table.PlanComparison { &.PlanTable__th__feature { padding: 0.75rem 1.25rem 0.75rem 3.25rem; font-weight: 600; + + &.PlanTable__th__feature--reduced_padding { + padding: 0.75rem 1.25rem; + } } &.PlanTable__th__last-feature { diff --git a/frontend/src/scenes/billing/PlanComparison.tsx b/frontend/src/scenes/billing/PlanComparison.tsx index 0fcd7bbf695ca..a2e75da9e5115 100644 --- a/frontend/src/scenes/billing/PlanComparison.tsx +++ b/frontend/src/scenes/billing/PlanComparison.tsx @@ -9,6 +9,7 @@ import { eventUsageLogic } from 'lib/utils/eventUsageLogic' import { convertLargeNumberToWords, getUpgradeProductLink } from './billing-utils' import { billingLogic } from './billingLogic' import { getProductIcon } from 'scenes/products/Products' +import useResizeObserver from 'use-resize-observer' export function PlanIcon({ feature, @@ -27,7 +28,7 @@ export function PlanIcon({ ) : feature.limit ? ( <> - + {feature.limit && `${convertLargeNumberToWords(feature.limit, null)} ${feature.unit && feature.unit}${ timeDenominator ? `/${timeDenominator}` : '' @@ -36,7 +37,7 @@ export function PlanIcon({ ) : ( <> - + {feature.note} )} @@ -48,6 +49,7 @@ const getProductTiers = ( plan: BillingV2PlanType, product: BillingProductV2Type | BillingProductV2AddonType ): JSX.Element => { + const { width, ref: tiersRef } = useResizeObserver() const tiers = plan?.tiers const allTierPrices = tiers?.map((tier) => parseFloat(tier.unit_amount_usd)) @@ -59,7 +61,8 @@ const getProductTiers = ( tiers?.map((tier, i) => (
{convertLargeNumberToWords(tier.up_to, tiers[i - 1]?.up_to, true, product.unit)} @@ -72,7 +75,11 @@ const getProductTiers = (
)) ) : product?.free_allocation ? ( -
+
Up to {convertLargeNumberToWords(product?.free_allocation, null)} {product?.unit}s/mo @@ -97,6 +104,7 @@ export const PlanComparison = ({ const fullyFeaturedPlan = plans[plans.length - 1] const { reportBillingUpgradeClicked } = useActions(eventUsageLogic) const { redirectPath, billing } = useValues(billingLogic) + const { width, ref: planComparisonRef } = useResizeObserver() const upgradeButtons = plans?.map((plan) => { return ( @@ -132,7 +140,7 @@ export const PlanComparison = ({ }) return ( - +
@@ -283,6 +291,10 @@ export const PlanComparison = ({
@@ -231,8 +239,8 @@ export const PlanComparison = ({ > {feature.name}
- Upgrade to paid + Subscribe ) } diff --git a/frontend/src/scenes/onboarding/OnboardingOtherProductsStep.tsx b/frontend/src/scenes/onboarding/OnboardingOtherProductsStep.tsx index 54618e56a0d64..6c3e92e9af68c 100644 --- a/frontend/src/scenes/onboarding/OnboardingOtherProductsStep.tsx +++ b/frontend/src/scenes/onboarding/OnboardingOtherProductsStep.tsx @@ -1,8 +1,8 @@ -import { LemonButton, LemonCard } from '@posthog/lemon-ui' +import { useWindowSize } from 'lib/hooks/useWindowSize' import { OnboardingStep } from './OnboardingStep' import { OnboardingStepKey, onboardingLogic } from './onboardingLogic' import { useActions, useValues } from 'kea' -import { getProductIcon } from 'scenes/products/Products' +import { ProductCard } from 'scenes/products/Products' export const OnboardingOtherProductsStep = ({ stepKey = OnboardingStepKey.OTHER_PRODUCTS, @@ -11,6 +11,8 @@ export const OnboardingOtherProductsStep = ({ }): JSX.Element => { const { product, suggestedProducts } = useValues(onboardingLogic) const { completeOnboarding } = useActions(onboardingLogic) + const { width } = useWindowSize() + const horizontalCard = width && width >= 640 return ( } stepKey={stepKey} > -
+
{suggestedProducts?.map((suggestedProduct) => ( - -
-
{getProductIcon(suggestedProduct.icon_key, 'text-2xl')}
-
-

{suggestedProduct.name}

-

{suggestedProduct.description}

-
-
-
- completeOnboarding(suggestedProduct.type)}> - Get started - -
-
+ getStartedActionOverride={() => completeOnboarding(suggestedProduct.type)} + orientation={horizontalCard ? 'horizontal' : 'vertical'} + className="w-full" + /> ))}
diff --git a/frontend/src/scenes/onboarding/OnboardingStep.tsx b/frontend/src/scenes/onboarding/OnboardingStep.tsx index a4bfdd92cbf42..171c607bf3d43 100644 --- a/frontend/src/scenes/onboarding/OnboardingStep.tsx +++ b/frontend/src/scenes/onboarding/OnboardingStep.tsx @@ -14,6 +14,7 @@ export const OnboardingStep = ({ showSkip = false, onSkip, continueOverride, + backActionOverride, }: { stepKey: OnboardingStepKey title: string @@ -22,6 +23,7 @@ export const OnboardingStep = ({ showSkip?: boolean onSkip?: () => void continueOverride?: JSX.Element + backActionOverride?: () => void }): JSX.Element => { const { hasNextStep, hasPreviousStep } = useValues(onboardingLogic) const { completeOnboarding, goToNextStep, goToPreviousStep } = useActions(onboardingLogic) @@ -39,14 +41,20 @@ export const OnboardingStep = ({
} - onClick={() => (hasPreviousStep ? goToPreviousStep() : router.actions.push(urls.products()))} + onClick={() => + backActionOverride + ? backActionOverride() + : hasPreviousStep + ? goToPreviousStep() + : router.actions.push(urls.products()) + } > Back
} > -
+

{title}

{subtitle}

{children} diff --git a/frontend/src/scenes/onboarding/sdks/SDKs.tsx b/frontend/src/scenes/onboarding/sdks/SDKs.tsx index 9a91ad06268d6..94556254fab58 100644 --- a/frontend/src/scenes/onboarding/sdks/SDKs.tsx +++ b/frontend/src/scenes/onboarding/sdks/SDKs.tsx @@ -8,6 +8,8 @@ import { useEffect } from 'react' import React from 'react' import { SDKInstructionsMap } from '~/types' import { InviteMembersButton } from '~/layout/navigation/TopBar/SitePopover' +import { IconArrowLeft } from '@posthog/icons' +import { useWindowSize } from 'lib/hooks/useWindowSize' export function SDKs({ usersAction, @@ -20,23 +22,37 @@ export function SDKs({ subtitle?: string stepKey?: OnboardingStepKey }): JSX.Element { - const { setSourceFilter, setSelectedSDK, setAvailableSDKInstructionsMap } = useActions(sdksLogic) - const { sourceFilter, sdks, selectedSDK, sourceOptions, showSourceOptionsSelect } = useValues(sdksLogic) + const { setSourceFilter, setSelectedSDK, setAvailableSDKInstructionsMap, setShowSideBySide, setPanel } = + useActions(sdksLogic) + const { sourceFilter, sdks, selectedSDK, sourceOptions, showSourceOptionsSelect, showSideBySide, panel } = + useValues(sdksLogic) const { productKey } = useValues(onboardingLogic) + const { width } = useWindowSize() + const minimumSideBySideSize = 768 useEffect(() => { setAvailableSDKInstructionsMap(sdkInstructionMap) }, []) + useEffect(() => { + width && setShowSideBySide(width > minimumSideBySideSize) + }, [width]) + return ( : undefined} + backActionOverride={!showSideBySide && panel === 'instructions' ? () => setPanel('options') : undefined} >
-
+
{showSourceOptionsSelect && (
{selectedSDK && productKey && !!sdkInstructionMap[selectedSDK.key] && ( -
+
+ {!showSideBySide && ( + } + onClick={() => setPanel('options')} + className="mb-8" + type="secondary" + > + View all SDKs + + )}
)} diff --git a/frontend/src/scenes/onboarding/sdks/sdksLogic.tsx b/frontend/src/scenes/onboarding/sdks/sdksLogic.tsx index 83203a1ef7bc4..2cfef0711bf10 100644 --- a/frontend/src/scenes/onboarding/sdks/sdksLogic.tsx +++ b/frontend/src/scenes/onboarding/sdks/sdksLogic.tsx @@ -40,6 +40,8 @@ export const sdksLogic = kea([ setSourceOptions: (sourceOptions: LemonSelectOptions) => ({ sourceOptions }), resetSDKs: true, setAvailableSDKInstructionsMap: (sdkInstructionMap: SDKInstructionsMap) => ({ sdkInstructionMap }), + setShowSideBySide: (showSideBySide: boolean) => ({ showSideBySide }), + setPanel: (panel: 'instructions' | 'options') => ({ panel }), }), reducers({ sourceFilter: [ @@ -72,6 +74,18 @@ export const sdksLogic = kea([ setAvailableSDKInstructionsMap: (_, { sdkInstructionMap }) => sdkInstructionMap, }, ], + showSideBySide: [ + null as boolean | null, + { + setShowSideBySide: (_, { showSideBySide }) => showSideBySide, + }, + ], + panel: [ + 'options' as 'instructions' | 'options', + { + setPanel: (_, { panel }) => panel, + }, + ], }), selectors({ showSourceOptionsSelect: [ @@ -100,7 +114,7 @@ export const sdksLogic = kea([ actions.filterSDKs() }, setSDKs: () => { - if (!values.selectedSDK) { + if (!values.selectedSDK && values.showSideBySide == true) { actions.setSelectedSDK(values.sdks?.[0] || null) } }, @@ -118,6 +132,16 @@ export const sdksLogic = kea([ actions.setSourceFilter(null) actions.setSourceOptions(getSourceOptions(values.availableSDKInstructionsMap)) }, + setSelectedSDK: () => { + if (values.selectedSDK) { + actions.setPanel('instructions') + } + }, + setShowSideBySide: () => { + if (values.showSideBySide && !values.selectedSDK) { + actions.setSelectedSDK(values.sdks?.[0] || null) + } + }, })), events(({ actions }) => ({ afterMount: () => { diff --git a/frontend/src/scenes/products/Products.tsx b/frontend/src/scenes/products/Products.tsx index 67efc964580ad..cba24e76c8a36 100644 --- a/frontend/src/scenes/products/Products.tsx +++ b/frontend/src/scenes/products/Products.tsx @@ -46,14 +46,26 @@ function OnboardingCompletedButton({ ) } -function OnboardingNotCompletedButton({ url, productKey }: { url: string; productKey: ProductKey }): JSX.Element { +function OnboardingNotCompletedButton({ + url, + productKey, + getStartedActionOverride, +}: { + url: string + productKey: ProductKey + getStartedActionOverride?: () => void +}): JSX.Element { const { onSelectProduct } = useActions(productsLogic) return ( { - onSelectProduct(productKey) - router.actions.push(url) + if (getStartedActionOverride) { + getStartedActionOverride() + } else { + onSelectProduct(productKey) + router.actions.push(url) + } }} > Get started @@ -65,19 +77,36 @@ export function getProductIcon(iconKey?: string | null, className?: string): JSX return Icons[iconKey || 'IconLogomark']({ className }) } -function ProductCard({ product }: { product: BillingProductV2Type }): JSX.Element { +export function ProductCard({ + product, + getStartedActionOverride, + orientation = 'vertical', + className, +}: { + product: BillingProductV2Type + getStartedActionOverride?: () => void + orientation?: 'horizontal' | 'vertical' + className?: string +}): JSX.Element { const { currentTeam } = useValues(teamLogic) const onboardingCompleted = currentTeam?.has_completed_onboarding_for?.[product.type] + const vertical = orientation === 'vertical' + return ( - -
-
{getProductIcon(product.icon_key, 'text-2xl')}
+ +
+
+
{getProductIcon(product.icon_key, 'text-2xl')}
+
-
-

{product.name}

+
+

{product.name}

+

{product.description}

-

{product.description}

-
+
{onboardingCompleted ? ( ) : ( - +
+ +
)}
diff --git a/frontend/src/scenes/session-recordings/player/inspector/components/ItemPerformanceEvent.tsx b/frontend/src/scenes/session-recordings/player/inspector/components/ItemPerformanceEvent.tsx index 4dbd3cd4b3f2c..96c4a9e01a143 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/components/ItemPerformanceEvent.tsx +++ b/frontend/src/scenes/session-recordings/player/inspector/components/ItemPerformanceEvent.tsx @@ -317,7 +317,7 @@ export function ItemPerformanceEvent({ ) : ( <> - +

Request started at{' '} @@ -466,29 +466,44 @@ function HeadersDisplay({ ) } -function StatusRow({ status }: { status: number | undefined }): JSX.Element | null { - if (status === undefined) { - return null - } +function StatusRow({ item }: { item: PerformanceEvent }): JSX.Element | null { + let statusRow = null + let methodRow = null + + if (item.response_status) { + const statusDescription = `${item.response_status} ${friendlyHttpStatus[item.response_status] || ''}` - const statusDescription = `${status} ${friendlyHttpStatus[status] || ''}` + let statusType: LemonTagType = 'success' + if (item.response_status >= 400 || item.response_status < 100) { + statusType = 'warning' + } else if (item.response_status >= 500) { + statusType = 'danger' + } - let statusType: LemonTagType = 'success' - if (status >= 400 || status < 100) { - statusType = 'warning' - } else if (status >= 500) { - statusType = 'danger' + statusRow = ( +

+
Status code
+ {statusDescription} +
+ ) } - return ( + if (item.method) { + methodRow = ( +
+
Request method
+
{item.method}
+
+ ) + } + + return methodRow || statusRow ? (

-
- Status code - {statusDescription} -
+ {methodRow} + {statusRow}

- ) + ) : null } diff --git a/frontend/src/scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming.tsx b/frontend/src/scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming.tsx index df65c02326529..f77aeadf524dd 100644 --- a/frontend/src/scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming.tsx +++ b/frontend/src/scenes/session-recordings/player/inspector/components/Timing/NetworkRequestTiming.tsx @@ -8,35 +8,6 @@ import { LemonDivider } from 'lib/lemon-ui/LemonDivider' import { SimpleKeyValueList } from 'scenes/session-recordings/player/inspector/components/SimpleKeyValueList' import { LemonButton } from 'lib/lemon-ui/LemonButton' -function colorForEntry(entryType: string | undefined): string { - switch (entryType) { - case 'domComplete': - return getSeriesColor(1) - case 'domInteractive': - return getSeriesColor(2) - case 'pageLoaded': - return getSeriesColor(3) - case 'first-contentful-paint': - return getSeriesColor(4) - case 'css': - return getSeriesColor(6) - case 'xmlhttprequest': - return getSeriesColor(7) - case 'fetch': - return getSeriesColor(8) - case 'other': - return getSeriesColor(9) - case 'script': - return getSeriesColor(10) - case 'link': - return getSeriesColor(11) - case 'first-paint': - return getSeriesColor(11) - default: - return getSeriesColor(13) - } -} - export interface EventPerformanceMeasure { start: number end: number @@ -50,7 +21,8 @@ const perfSections = [ 'dns lookup', 'connection time', 'tls time', - 'waiting for first byte (TTFB)', + 'request queuing time', + 'waiting for first byte', 'receiving response', 'document processing', ] as const @@ -62,7 +34,9 @@ const perfDescriptions: Record<(typeof perfSections)[number], string> = { 'dns lookup': 'The time taken to complete any DNS lookup for the resource.', 'connection time': 'The time taken to establish a connection to the server to retrieve the resource.', 'tls time': 'The time taken for the SSL/TLS handshake.', - 'waiting for first byte (TTFB)': 'The time taken waiting for the server to start returning a response.', + 'request queuing time': "The time taken waiting in the browser's task queue once ready to make a request.", + 'waiting for first byte': + 'The time taken waiting for the server to start returning a response. Also known as TTFB or time to first byte.', 'receiving response': 'The time taken to receive the response from the server.', 'document processing': 'The time taken to process the document after the response from the server has been received.', @@ -71,29 +45,36 @@ const perfDescriptions: Record<(typeof perfSections)[number], string> = { function colorForSection(section: (typeof perfSections)[number]): string { switch (section) { case 'redirect': - return getSeriesColor(1) - case 'app cache': return getSeriesColor(2) - case 'dns lookup': + case 'app cache': return getSeriesColor(3) - case 'connection time': + case 'dns lookup': return getSeriesColor(4) + case 'connection time': + return getSeriesColor(5) case 'tls time': return getSeriesColor(6) - case 'waiting for first byte (TTFB)': + case 'request queuing time': return getSeriesColor(7) - case 'receiving response': + case 'waiting for first byte': return getSeriesColor(8) - case 'document processing': + case 'receiving response': return getSeriesColor(9) - default: + case 'document processing': return getSeriesColor(10) + default: + return getSeriesColor(11) } } /** * There are defined sections to performance measurement. We may have data for some or all of them * + * + * 0) Queueing + * - from start_time + * - until the first item with activity + * * 1) Redirect * - from startTime which would also be redirectStart * - until redirect_end @@ -127,9 +108,6 @@ function colorForSection(section: (typeof perfSections)[number]): string { * - until load_event_end * * see https://nicj.net/resourcetiming-in-practice/ - * - * @param perfEntry - * @param maxTime */ function calculatePerformanceParts(perfEntry: PerformanceEvent): Record { const performanceParts: Record = {} @@ -138,7 +116,7 @@ function calculatePerformanceParts(perfEntry: PerformanceEvent): Record 1) { // find in eventsMapping[capturedRequest.url][capturedRequest.startTime] by matching capturedRequest.endTime and element.response_end const matchedEndTime = matchedStartTime.find( @@ -135,6 +136,7 @@ export function matchNetworkEvents(snapshotsByWindowId: Record request_body?: Body response_body?: Body + method?: string } export interface CurrentBillCycleType {