refactor: Tiny fixes to Revenue Analytics (#37785)

Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Rafael Audibert
2025-09-09 09:08:22 -03:00
committed by GitHub
parent 6f336cda3a
commit daef2253e7
15 changed files with 130 additions and 96 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 186 KiB

After

Width:  |  Height:  |  Size: 186 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 223 KiB

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 222 KiB

After

Width:  |  Height:  |  Size: 221 KiB

View File

@@ -11,6 +11,16 @@
&.Spinner--textColored {
--spinner-color: currentColor;
}
&.Spinner--medium {
width: 1.5em;
height: 1.5em;
}
&.Spinner--large {
width: 30px;
height: 30px;
}
}
.Spinner__layer {

View File

@@ -32,6 +32,7 @@ export interface SpinnerProps {
className?: string
speed?: `${number}s` // Seconds
captureTime?: boolean
size?: 'small' | 'medium' | 'large'
}
/** Smoothly animated spinner for loading states. It does not indicate progress, only that something's happening. */
@@ -40,6 +41,7 @@ export function Spinner({
className,
speed = '1s',
captureTime = true,
size = 'small',
}: SpinnerProps): JSX.Element {
useTimingCapture(captureTime)
@@ -47,7 +49,12 @@ export function Spinner({
<svg
// eslint-disable-next-line react/forbid-dom-props
style={{ '--spinner-speed': speed } as React.CSSProperties}
className={twMerge('LemonIcon Spinner', textColored && `Spinner--textColored`, className)}
className={twMerge(
'LemonIcon Spinner',
textColored && `Spinner--textColored`,
size && `Spinner--${size}`,
className
)}
viewBox="0 0 48 48"
xmlns="http://www.w3.org/2000/svg"
>

View File

@@ -11,7 +11,7 @@ import { NEW_QUERY, QueryTab, multitabEditorLogic } from './multitabEditorLogic'
interface QueryTabsProps {
models: QueryTab[]
onClick: (model: QueryTab) => void
onClear: (model: QueryTab) => void
onClear: (model: QueryTab, options?: { force?: boolean }) => void
onRename: (model: QueryTab, newName: string) => void
onAdd: () => void
activeModelUri: QueryTab | null
@@ -64,7 +64,7 @@ export function QueryTabs({ models, onClear, onClick, onAdd, onRename, activeMod
interface QueryTabProps {
model: QueryTab
onClick: (model: QueryTab) => void
onClear?: (model: QueryTab) => void
onClear?: (model: QueryTab, options?: { force?: boolean }) => void
active: boolean
onRename: (model: QueryTab, newName: string) => void
}
@@ -125,7 +125,8 @@ function QueryTabComponent({ model, active, onClear, onClick, onRename }: QueryT
<LemonButton
onClick={(e) => {
e.stopPropagation()
onClear(model)
onClear(model, { force: !!e.shiftKey })
}}
size="xsmall"
icon={<IconX />}

View File

@@ -228,7 +228,7 @@ export const multitabEditorLogic = kea<multitabEditorLogicType>([
draft,
}),
loadUpstream: (modelId: string) => ({ modelId }),
deleteTab: (tab: QueryTab) => ({ tab }),
deleteTab: (tab: QueryTab, options?: { force?: boolean }) => ({ tab, options }),
_deleteTab: (tab: QueryTab) => ({ tab }),
removeTab: (tab: QueryTab) => ({ tab }),
selectTab: (tab: QueryTab) => ({ tab }),
@@ -788,12 +788,18 @@ export const multitabEditorLogic = kea<multitabEditorLogicType>([
})
}
},
deleteTab: ({ tab: tabToRemove }) => {
deleteTab: ({ tab: tabToRemove, options: { force } = {} }) => {
if (force) {
actions._deleteTab(tabToRemove)
return
}
if (
(values.activeModelUri?.view && values.queryInput !== values.sourceQuery.source.query) ||
(values.activeModelUri?.draft && values.queryInput !== tabToRemove.draft?.query.query)
) {
const viewOrDraft = values.activeModelUri?.draft ? 'draft' : 'view'
LemonDialog.open({
title: 'Close tab',
description: `Are you sure you want to close this ${viewOrDraft}? There are unsaved changes.`,

View File

@@ -202,28 +202,6 @@ class DataWarehouseJoin(CreatedMetaFields, UUIDTModel, DeletedMetaFields):
return _join_function_for_experiments
def join_for_persons_revenue_analytics_table(self) -> ast.JoinExpr:
from posthog.hogql import ast
left = self.__parse_table_key_expression(self.source_table_key, self.source_table_name)
right = self.__parse_table_key_expression(self.joining_table_key, self.joining_table_name)
join_expr = ast.JoinExpr(
table=ast.Field(chain=self.joining_table_name_chain),
join_type="LEFT JOIN",
alias=self.joining_table_name,
constraint=ast.JoinConstraint(
expr=ast.CompareOperation(
op=ast.CompareOperationOp.Eq,
left=left,
right=right,
),
constraint_type="ON",
),
)
return join_expr
def __parse_table_key_expression(self, table_key: str, table_name: str) -> ast.Expr:
expr = parse_expr(table_key)
if isinstance(expr, ast.Field):

View File

@@ -71,7 +71,7 @@ export function InlineSetup({ initialSetupView }: InlineSetupProps): JSX.Element
return (
<div className="space-y-6">
{/* Main Setup Card */}
<LemonCard className="border-2 border-dashed border-border" hoverEffect={false}>
<LemonCard hoverEffect={false}>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
@@ -93,8 +93,8 @@ export function InlineSetup({ initialSetupView }: InlineSetupProps): JSX.Element
{/* Current Status */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Events Status */}
<div className="flex items-center gap-3 p-3 rounded-lg border border-border">
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-white border border-border">
<div className="flex items-center gap-3 p-3 rounded-lg border border-primary">
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-bg-light border border-primary">
{hasEvents ? (
<IconCheckCircle className="w-6 h-6" />
) : (
@@ -114,8 +114,8 @@ export function InlineSetup({ initialSetupView }: InlineSetupProps): JSX.Element
</div>
{/* Sources Status */}
<div className="flex items-center gap-3 p-3 rounded-lg border border-border">
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-white border border-border">
<div className="flex items-center gap-3 p-3 rounded-lg border border-primary">
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-bg-light border border-primary">
{hasSources ? (
<IconCheckCircle className="w-6 h-6" />
) : (
@@ -136,7 +136,7 @@ export function InlineSetup({ initialSetupView }: InlineSetupProps): JSX.Element
</div>
{/* Action Buttons */}
<div className="flex flex-col sm:flex-row gap-3 pt-2 border-t border-border">
<div className="flex flex-col sm:flex-row gap-3 pt-2 border-t border-primary">
<LemonButton
type="primary"
icon={<IconPlus />}
@@ -167,7 +167,7 @@ export function InlineSetup({ initialSetupView }: InlineSetupProps): JSX.Element
<LemonCard hoverEffect={false}>
<div className="space-y-4">
<div className="flex items-center gap-3">
<div className="flex items-center justify-center w-12 h-12 rounded-full bg-white border border-border">
<div className="flex items-center justify-center w-12 h-12 rounded-full bg-white border border-primary">
<IconDatabase className="w-7 h-7" style={{ color: 'var(--primary-3000)' }} />
</div>
<div>
@@ -185,8 +185,8 @@ export function InlineSetup({ initialSetupView }: InlineSetupProps): JSX.Element
source.isAvailable
? source.isConnected
? 'border-primary bg-primary-lightest'
: 'border-border bg-bg-light'
: 'border-border bg-bg-light opacity-60',
: 'border-primary bg-bg-light'
: 'border-primary bg-bg-light opacity-60',
source.isAvailable ? 'cursor-pointer' : 'cursor-not-allowed'
)}
onClick={source.isAvailable ? () => handleSourceSelect(source.id) : undefined}

View File

@@ -1,19 +1,22 @@
import { useActions, useValues } from 'kea'
import { router } from 'kea-router'
import { IconInfo, IconPlus } from '@posthog/icons'
import { LemonButton, LemonDivider, LemonSwitch, Link, Spinner, Tooltip } from '@posthog/lemon-ui'
import { IconInfo, IconPlus, IconTrash } from '@posthog/icons'
import { LemonButton, LemonDivider, LemonSwitch, Link, Spinner, Tooltip, lemonToast } from '@posthog/lemon-ui'
import api from 'lib/api'
import { useFeatureFlag } from 'lib/hooks/useFeatureFlag'
import { LemonTable } from 'lib/lemon-ui/LemonTable'
import { cn } from 'lib/utils/css-classes'
import { deleteWithUndo } from 'lib/utils/deleteWithUndo'
import { ViewLinkModal } from 'scenes/data-warehouse/ViewLinkModal'
import { queryDatabaseLogic } from 'scenes/data-warehouse/editor/sidebar/queryDatabaseLogic'
import { DataWarehouseSourceIcon } from 'scenes/data-warehouse/settings/DataWarehouseSourceIcon'
import { viewLinkLogic } from 'scenes/data-warehouse/viewLinkLogic'
import { urls } from 'scenes/urls'
import { SceneSection } from '~/layout/scenes/components/SceneSection'
import { ExternalDataSource, PipelineNodeTab, PipelineStage } from '~/types'
import { DataWarehouseViewLink, ExternalDataSource, PipelineNodeTab, PipelineStage } from '~/types'
import { revenueAnalyticsSettingsLogic } from './revenueAnalyticsSettingsLogic'
@@ -27,6 +30,8 @@ export function ExternalDataSourceConfiguration({
const { dataWarehouseSources, dataWarehouseSourcesLoading, joins } = useValues(revenueAnalyticsSettingsLogic)
const { updateSourceRevenueAnalyticsConfig } = useActions(revenueAnalyticsSettingsLogic)
const { toggleEditJoinModal, toggleNewJoinModal } = useActions(viewLinkLogic)
const { loadDatabase, loadJoins } = useActions(queryDatabaseLogic)
const newSceneLayout = useFeatureFlag('NEW_SCENE_LAYOUT')
const revenueSources =
dataWarehouseSources?.results.filter((source) => VALID_REVENUE_SOURCES.includes(source.source_type)) ?? []
@@ -41,6 +46,22 @@ export function ExternalDataSourceConfiguration({
return undefined
}
const deleteJoin = (join: DataWarehouseViewLink): void => {
void deleteWithUndo({
endpoint: api.dataWarehouseViewLinks.determineDeleteEndpoint(),
object: {
id: join.id,
name: `${join.field_name} on ${join.source_table_name}`,
},
callback: () => {
loadDatabase()
loadJoins()
},
}).catch((e) => {
lemonToast.error(`Failed to delete warehouse view link: ${e.detail}`)
})
}
return (
<SceneSection
hideTitleAndDescription={!newSceneLayout}
@@ -86,6 +107,10 @@ export function ExternalDataSourceConfiguration({
title: '',
width: 0,
render: (_, source: ExternalDataSource) => {
if (dataWarehouseSourcesLoading) {
return <Spinner size="medium" />
}
return <DataWarehouseSourceIcon type={source.source_type} />
},
},
@@ -94,15 +119,27 @@ export function ExternalDataSourceConfiguration({
title: 'Source',
render: (_, source: ExternalDataSource) => {
return (
<Link
to={urls.pipelineNode(
PipelineStage.Source,
`managed-${source.id}`,
PipelineNodeTab.Schemas
)}
>
{source.source_type}&nbsp;{source.prefix && `(${source.prefix})`}
</Link>
<span className="inline-flex items-centet gap-2">
<Link
to={urls.pipelineNode(
PipelineStage.Source,
`managed-${source.id}`,
PipelineNodeTab.Schemas
)}
>
{source.source_type}&nbsp;{source.prefix && `(${source.prefix})`}
</Link>
<LemonSwitch
checked={source.revenue_analytics_config.enabled}
disabledReason={dataWarehouseSourcesLoading ? 'Updating...' : undefined}
onChange={(checked) =>
updateSourceRevenueAnalyticsConfig({
source,
config: { enabled: checked },
})
}
/>
</span>
)
},
},
@@ -131,15 +168,26 @@ export function ExternalDataSourceConfiguration({
Joined to <code>persons</code> via:
</span>
{join ? (
<LemonButton
type="secondary"
size="small"
onClick={() => toggleEditJoinModal(join)}
disabledReason={disabledReasonForRevenueAnalyticsConfig(source)}
>
{join.source_table_name}.{join.source_table_key}
</LemonButton>
{join && source.revenue_analytics_config.enabled ? (
<>
<LemonButton
type="secondary"
size="small"
onClick={() => toggleEditJoinModal(join)}
disabledReason={disabledReasonForRevenueAnalyticsConfig(source)}
>
{join.source_table_name}.{join.source_table_key}
</LemonButton>
<LemonButton
type="secondary"
status="danger"
size="small"
tooltip="Delete join"
icon={<IconTrash />}
onClick={() => deleteJoin(join)}
/>
</>
) : (
<LemonButton
type="secondary"
@@ -190,15 +238,27 @@ export function ExternalDataSourceConfiguration({
Joined to <code>groups</code> via:
</span>
{join ? (
<LemonButton
type="secondary"
size="small"
onClick={() => toggleEditJoinModal(join)}
disabledReason={disabledReasonForRevenueAnalyticsConfig(source)}
>
{join.source_table_name}.{join.source_table_key}
</LemonButton>
{join && source.revenue_analytics_config.enabled ? (
<>
<LemonButton
type="secondary"
size="small"
onClick={() => toggleEditJoinModal(join)}
disabledReason={disabledReasonForRevenueAnalyticsConfig(source)}
tooltip="Edit join"
>
{join.source_table_name}.{join.source_table_key}
</LemonButton>
<LemonButton
type="secondary"
status="danger"
size="small"
tooltip="Delete join"
icon={<IconTrash />}
onClick={() => deleteJoin(join)}
/>
</>
) : (
<LemonButton
type="secondary"
@@ -211,7 +271,7 @@ export function ExternalDataSourceConfiguration({
source_table_name: joinName,
source_table_key: 'id',
joining_table_name: 'groups',
joining_table_key: 'group_key',
joining_table_key: 'key',
field_name: 'groups',
})
}
@@ -224,24 +284,6 @@ export function ExternalDataSourceConfiguration({
)
},
},
{
key: 'revenue_analytics_enabled',
title: 'Enabled?',
render: (_, source: ExternalDataSource) => {
return (
<LemonSwitch
checked={source.revenue_analytics_config.enabled}
disabledReason={dataWarehouseSourcesLoading ? 'Updating...' : undefined}
onChange={(checked) =>
updateSourceRevenueAnalyticsConfig({
source,
config: { enabled: checked },
})
}
/>
)
},
},
{
key: 'separator',
title: <LemonDivider vertical className="py-1 h-[16px]" />,
@@ -274,16 +316,6 @@ export function ExternalDataSourceConfiguration({
)
},
},
{
key: 'loading',
render: () => {
if (!dataWarehouseSourcesLoading) {
return null
}
return <Spinner />
},
},
]}
/>