refactor: Tiny fixes to Revenue Analytics (#37785)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
|
Before Width: | Height: | Size: 186 KiB After Width: | Height: | Size: 186 KiB |
|
Before Width: | Height: | Size: 184 KiB After Width: | Height: | Size: 184 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 223 KiB After Width: | Height: | Size: 221 KiB |
|
Before Width: | Height: | Size: 222 KiB After Width: | Height: | Size: 221 KiB |
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
>
|
||||
|
||||
@@ -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 />}
|
||||
|
||||
@@ -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.`,
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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} {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} {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 />
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
|
||||