mirror of
https://github.com/PostHog/posthog.git
synced 2024-11-24 00:47:50 +01:00
Revert "Revert "Continuation - Frontend - Refactor event & properties for async server-side loading (#4078)" (#4092)" (#4093)
This reverts commit d67ab843f1
.
This commit is contained in:
parent
c0bdcb8556
commit
a7352b4105
@ -1,5 +1,6 @@
|
||||
# This file contains a list of 'ignored' typescript errors. If any new ones are added they will cause ci breakage
|
||||
# Add lines via bin/check-typescript-strict | cut -d":" -f1
|
||||
# You can get an updated list of blacklisted errors by running bin/check-typescript-strict | cut -d":" -f1
|
||||
# You will need to manually update this file if applicable.
|
||||
Type.ts
|
||||
frontend/src/initKea.ts(2,32)
|
||||
frontend/src/initKea.ts(4,31)
|
||||
@ -60,13 +61,6 @@ frontend/src/scenes/sessions/SessionDetails.tsx(55,51)
|
||||
frontend/src/scenes/sessions/SessionsView.tsx(199,29)
|
||||
frontend/src/scenes/sessions/filters/DurationFilter.tsx(27,34)
|
||||
frontend/src/scenes/sessions/filters/EventPropertyFilter.tsx(21,41)
|
||||
frontend/src/scenes/sessions/filters/SessionsFilterBox.tsx(43,13)
|
||||
frontend/src/scenes/sessions/filters/SessionsFilterBox.tsx(44,13)
|
||||
frontend/src/scenes/sessions/filters/SessionsFilterBox.tsx(78,13)
|
||||
frontend/src/scenes/sessions/filters/SessionsFilterBox.tsx(79,13)
|
||||
frontend/src/scenes/sessions/filters/SessionsFilterBox.tsx(87,13)
|
||||
frontend/src/scenes/sessions/filters/SessionsFilterBox.tsx(116,13)
|
||||
frontend/src/scenes/sessions/filters/SessionsFilterBox.tsx(173,9)
|
||||
frontend/src/scenes/sessions/sessionsPlayLogic.ts(155,61)
|
||||
frontend/src/toolbar/ToolbarApp.tsx(35,74)
|
||||
frontend/src/toolbar/actions/SelectorCount.tsx(2,38)
|
||||
|
@ -3,13 +3,11 @@ import Fuse from 'fuse.js'
|
||||
import { Select } from 'antd'
|
||||
import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo'
|
||||
import { SelectGradientOverflow } from 'lib/components/SelectGradientOverflow'
|
||||
import { EventProperty } from 'scenes/teamLogic'
|
||||
|
||||
type PropertyOption = EventProperty
|
||||
import { SelectOption } from '~/types'
|
||||
|
||||
interface Props {
|
||||
optionGroups: Array<PropertyOptionGroup>
|
||||
value: Partial<PropertyOption> | null
|
||||
value: Partial<SelectOption> | null
|
||||
onChange: (type: PropertyOptionGroup['type'], value: string) => void
|
||||
placeholder: string
|
||||
autoOpenIfEmpty?: boolean
|
||||
|
@ -2,7 +2,7 @@ import { kea } from 'kea'
|
||||
import api from 'lib/api'
|
||||
import { objectsEqual } from 'lib/utils'
|
||||
import { router } from 'kea-router'
|
||||
import { teamLogic } from 'scenes/teamLogic'
|
||||
import { propertyDefinitionsLogic } from 'scenes/events/propertyDefinitionsLogic'
|
||||
|
||||
export function parseProperties(input) {
|
||||
if (Array.isArray(input) || !input) {
|
||||
@ -103,11 +103,11 @@ export const propertyFilterLogic = kea({
|
||||
}
|
||||
}
|
||||
},
|
||||
[teamLogic.actionTypes.loadCurrentTeamSuccess]: async () => {
|
||||
/* Set the event properties in case the `currentTeam` request came later, or the event
|
||||
[propertyDefinitionsLogic.actionTypes.loadPropertyDefinitionsSuccess]: async () => {
|
||||
/* Set the event properties in case the `loadPropertyDefinitions` request came later, or the event
|
||||
properties were updated. */
|
||||
if (props.endpoint !== 'person' && props.endpoint !== 'sessions') {
|
||||
actions.setProperties(teamLogic.values.eventProperties)
|
||||
actions.setProperties(propertyDefinitionsLogic.values.transformedPropertyDefinitions)
|
||||
}
|
||||
},
|
||||
}),
|
||||
@ -136,7 +136,7 @@ export const propertyFilterLogic = kea({
|
||||
}),
|
||||
|
||||
selectors: {
|
||||
filtersLoading: [() => [teamLogic.selectors.currentTeamLoading], (currentTeamLoading) => currentTeamLoading],
|
||||
filtersLoading: [() => [propertyDefinitionsLogic.selectors.loaded], (loaded) => !loaded],
|
||||
},
|
||||
|
||||
events: ({ actions, props }) => ({
|
||||
@ -145,7 +145,7 @@ export const propertyFilterLogic = kea({
|
||||
actions.loadPersonProperties()
|
||||
// TODO: Event properties in sessions is temporarily unsupported (context https://github.com/PostHog/posthog/issues/2735)
|
||||
if (props.endpoint !== 'person' && props.endpoint !== 'sessions') {
|
||||
actions.setProperties(teamLogic.values.eventProperties)
|
||||
actions.setProperties(propertyDefinitionsLogic.values.transformedPropertyDefinitions)
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
@ -1,7 +1,13 @@
|
||||
import React from 'react'
|
||||
import { Popover } from 'antd'
|
||||
import { KeyMapping } from '~/types'
|
||||
|
||||
export const keyMapping = {
|
||||
export interface KeyMappingInterface {
|
||||
event: Record<string, KeyMapping>
|
||||
element: Record<string, KeyMapping>
|
||||
}
|
||||
|
||||
export const keyMapping: KeyMappingInterface = {
|
||||
event: {
|
||||
$timestamp: {
|
||||
label: 'Timestamp',
|
||||
@ -357,12 +363,12 @@ export const keyMapping = {
|
||||
$geoip_latitude: {
|
||||
label: 'Latitude',
|
||||
description: `Approximated latitude matched to this event's IP address.`,
|
||||
examples: [-33.8591, 13.1337],
|
||||
examples: ['-33.8591', '13.1337'],
|
||||
},
|
||||
$geoip_longitude: {
|
||||
label: 'Longitude',
|
||||
description: `Approximated longitude matched to this event's IP address.`,
|
||||
examples: [151.2, 80.8008],
|
||||
examples: ['151.2', '80.8008'],
|
||||
},
|
||||
$geoip_time_zone: {
|
||||
label: 'Timezone',
|
||||
@ -427,16 +433,17 @@ export const keyMapping = {
|
||||
},
|
||||
}
|
||||
|
||||
export function PropertyKeyInfo({ value, type = 'event' }) {
|
||||
let data
|
||||
if (type === 'cohort') {
|
||||
return value.name
|
||||
} else {
|
||||
data = keyMapping[type][value]
|
||||
}
|
||||
interface PropertyKeyInfoInterface {
|
||||
value: string
|
||||
type?: 'event' | 'element'
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return value
|
||||
export function PropertyKeyInfo({ value, type = 'event' }: PropertyKeyInfoInterface): JSX.Element {
|
||||
let data = null
|
||||
if (value in keyMapping[type]) {
|
||||
data = keyMapping[type][value]
|
||||
} else {
|
||||
return <>{value}</>
|
||||
}
|
||||
|
||||
return (
|
@ -19,14 +19,14 @@ export interface SelectBoxItem {
|
||||
}
|
||||
|
||||
export interface SelectedItem {
|
||||
id?: number // Populated for actions
|
||||
id?: number | string // Populated for actions (string is used for UUIDs)
|
||||
name: string
|
||||
key: string
|
||||
value?: string
|
||||
action?: ActionType
|
||||
event?: string
|
||||
volume?: number
|
||||
usage_count?: number
|
||||
volume_30_day?: number | null // Only for properties or events
|
||||
query_usage_30_day?: number | null // Only for properties or events
|
||||
is_numerical?: boolean // Only for properties
|
||||
category?: string
|
||||
cohort?: CohortType
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ import { featureFlagLogic } from './logic/featureFlagLogic'
|
||||
import { open } from '@papercups-io/chat-widget'
|
||||
import posthog from 'posthog-js'
|
||||
import { WEBHOOK_SERVICES } from 'lib/constants'
|
||||
import { KeyMappingInterface } from 'lib/components/PropertyKeyInfo'
|
||||
|
||||
const SI_PREFIXES: { value: number; symbol: string }[] = [
|
||||
{ value: 1e18, symbol: 'E' },
|
||||
@ -269,7 +270,7 @@ export function isValidRegex(value: string): boolean {
|
||||
export function formatPropertyLabel(
|
||||
item: Record<string, any>,
|
||||
cohorts: Record<string, any>[],
|
||||
keyMapping: Record<string, Record<string, any>>
|
||||
keyMapping: KeyMappingInterface
|
||||
): string {
|
||||
const { value, key, operator, type } = item
|
||||
return type === 'cohort'
|
||||
@ -535,7 +536,7 @@ export function dateFilterToText(
|
||||
return name
|
||||
}
|
||||
|
||||
export function humanizeNumber(number: number, digits: number = 1): string {
|
||||
export function humanizeNumber(number: number | null, digits: number = 1): string {
|
||||
if (number === null) {
|
||||
return '-'
|
||||
}
|
||||
|
@ -76,7 +76,6 @@ export const preflightLogic = kea<preflightLogicType<PreflightStatus, PreflightM
|
||||
}),
|
||||
urlToAction: ({ actions }) => ({
|
||||
'/preflight': (_: any, { mode }: { mode: PreflightMode | null }) => {
|
||||
console.log(mode)
|
||||
if (mode) {
|
||||
actions.setPreflightMode(mode, true)
|
||||
}
|
||||
|
@ -1,11 +1,17 @@
|
||||
import React from 'react'
|
||||
import { Select } from 'antd'
|
||||
import { useValues } from 'kea'
|
||||
import { teamLogic } from 'scenes/teamLogic'
|
||||
import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo'
|
||||
import { eventDefinitionsLogic } from 'scenes/events/eventDefinitionsLogic'
|
||||
|
||||
export function EventName({ value, onChange, isActionStep = false }) {
|
||||
const { eventNamesGrouped } = useValues(teamLogic)
|
||||
interface EventNameInterface {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
isActionStep?: boolean
|
||||
}
|
||||
|
||||
export function EventName({ value, onChange, isActionStep = false }: EventNameInterface): JSX.Element {
|
||||
const { eventNamesGrouped } = useValues(eventDefinitionsLogic)
|
||||
|
||||
return (
|
||||
<span>
|
@ -1,49 +1,53 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useValues } from 'kea'
|
||||
import { Alert, Input, Table, Tooltip } from 'antd'
|
||||
import { Alert, Input, Skeleton, Table, Tooltip } from 'antd'
|
||||
import Fuse from 'fuse.js'
|
||||
import { InfoCircleOutlined, WarningOutlined } from '@ant-design/icons'
|
||||
import { humanizeNumber } from 'lib/utils'
|
||||
import { teamLogic } from 'scenes/teamLogic'
|
||||
import { capitalizeFirstLetter, humanizeNumber } from 'lib/utils'
|
||||
import { preflightLogic } from 'scenes/PreflightCheck/logic'
|
||||
import { ColumnsType } from 'antd/lib/table'
|
||||
import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo'
|
||||
|
||||
export interface EventOrPropType {
|
||||
event?: string
|
||||
key?: string
|
||||
usage_count: number
|
||||
volume: number
|
||||
warnings: string[]
|
||||
}
|
||||
import { eventDefinitionsLogic } from './eventDefinitionsLogic'
|
||||
import { EventDefinition, PropertyDefinition } from '~/types'
|
||||
|
||||
type EventTableType = 'event' | 'property'
|
||||
|
||||
const searchEvents = (sources: EventOrPropType[], search: string, key: EventTableType): EventOrPropType[] => {
|
||||
type EventOrPropType = EventDefinition & PropertyDefinition
|
||||
|
||||
interface VolumeTableRecord {
|
||||
eventOrProp: EventOrPropType
|
||||
warnings: string[]
|
||||
}
|
||||
|
||||
const search = (sources: VolumeTableRecord[], searchQuery: string): VolumeTableRecord[] => {
|
||||
return new Fuse(sources, {
|
||||
keys: [key],
|
||||
keys: ['eventOrProp.name'],
|
||||
threshold: 0.3,
|
||||
})
|
||||
.search(search)
|
||||
.search(searchQuery)
|
||||
.map((result) => result.item)
|
||||
}
|
||||
|
||||
export function VolumeTable({ type, data }: { type: EventTableType; data: EventOrPropType[] }): JSX.Element {
|
||||
export function VolumeTable({
|
||||
type,
|
||||
data,
|
||||
}: {
|
||||
type: EventTableType
|
||||
data: Array<EventDefinition | PropertyDefinition>
|
||||
}): JSX.Element {
|
||||
const [searchTerm, setSearchTerm] = useState(false as string | false)
|
||||
const [dataWithWarnings, setDataWithWarnings] = useState([] as EventOrPropType[])
|
||||
const [dataWithWarnings, setDataWithWarnings] = useState([] as VolumeTableRecord[])
|
||||
|
||||
const key = type === 'property' ? 'key' : type // Properties are stored under `key`
|
||||
|
||||
const columns: ColumnsType<EventOrPropType> = [
|
||||
const columns: ColumnsType<VolumeTableRecord> = [
|
||||
{
|
||||
title: type,
|
||||
render: function RenderEvent(item: EventOrPropType): JSX.Element {
|
||||
title: `${capitalizeFirstLetter(type)} name`,
|
||||
render: function Render(_, record): JSX.Element {
|
||||
return (
|
||||
<span>
|
||||
<span className="ph-no-capture">
|
||||
<PropertyKeyInfo value={item[key]} />
|
||||
<PropertyKeyInfo value={record.eventOrProp.name} />
|
||||
</span>
|
||||
{item.warnings?.map((warning) => (
|
||||
{record.warnings?.map((warning) => (
|
||||
<Tooltip
|
||||
key={warning}
|
||||
color="orange"
|
||||
@ -59,7 +63,7 @@ export function VolumeTable({ type, data }: { type: EventTableType; data: EventO
|
||||
</span>
|
||||
)
|
||||
},
|
||||
sorter: (a: EventOrPropType, b: EventOrPropType) => ('' + a[key]).localeCompare(b[key] || ''),
|
||||
sorter: (a, b) => ('' + a.eventOrProp.name).localeCompare(b.eventOrProp.name || ''),
|
||||
filters: [
|
||||
{ text: 'Has warnings', value: 'warnings' },
|
||||
{ text: 'No warnings', value: 'noWarnings' },
|
||||
@ -78,11 +82,13 @@ export function VolumeTable({ type, data }: { type: EventTableType; data: EventO
|
||||
</Tooltip>
|
||||
)
|
||||
},
|
||||
render: function RenderVolume(item: EventOrPropType) {
|
||||
return <span className="ph-no-capture">{humanizeNumber(item.volume)}</span>
|
||||
render: function RenderVolume(_, record) {
|
||||
return <span className="ph-no-capture">{humanizeNumber(record.eventOrProp.volume_30_day)}</span>
|
||||
},
|
||||
sorter: (a: EventOrPropType, b: EventOrPropType) =>
|
||||
a.volume == b.volume ? a.usage_count - b.usage_count : a.volume - b.volume,
|
||||
sorter: (a, b) =>
|
||||
a.eventOrProp.volume_30_day == b.eventOrProp.volume_30_day
|
||||
? (a.eventOrProp.volume_30_day || -1) - (b.eventOrProp.volume_30_day || -1)
|
||||
: (a.eventOrProp.volume_30_day || -1) - (b.eventOrProp.volume_30_day || -1),
|
||||
},
|
||||
{
|
||||
title: function QueriesTitle() {
|
||||
@ -96,30 +102,34 @@ export function VolumeTable({ type, data }: { type: EventTableType; data: EventO
|
||||
</Tooltip>
|
||||
)
|
||||
},
|
||||
// eslint-disable-next-line react/display-name
|
||||
render: (item: EventOrPropType) => (
|
||||
<span className="ph-no-capture">{humanizeNumber(item.usage_count)}</span>
|
||||
),
|
||||
sorter: (a: EventOrPropType, b: EventOrPropType) =>
|
||||
a.usage_count == b.usage_count ? a.volume - b.volume : a.usage_count - b.usage_count,
|
||||
render: function Render(_, item) {
|
||||
return <span className="ph-no-capture">{humanizeNumber(item.eventOrProp.query_usage_30_day)}</span>
|
||||
},
|
||||
sorter: (a, b) =>
|
||||
a.eventOrProp.query_usage_30_day == b.eventOrProp.query_usage_30_day
|
||||
? (a.eventOrProp.query_usage_30_day || -1) - (b.eventOrProp.query_usage_30_day || -1)
|
||||
: (a.eventOrProp.query_usage_30_day || -1) - (b.eventOrProp.query_usage_30_day || -1),
|
||||
},
|
||||
]
|
||||
|
||||
useEffect(() => {
|
||||
setDataWithWarnings(
|
||||
data.map(
|
||||
(item): EventOrPropType => {
|
||||
item.warnings = []
|
||||
if (item[key]?.endsWith(' ')) {
|
||||
item.warnings.push(`This ${type} ends with a space.`)
|
||||
(eventOrProp: EventOrPropType): VolumeTableRecord => {
|
||||
const record = { eventOrProp } as VolumeTableRecord
|
||||
record.warnings = []
|
||||
if (eventOrProp.name?.endsWith(' ')) {
|
||||
record.warnings.push(`This ${type} ends with a space.`)
|
||||
}
|
||||
if (item[key]?.startsWith(' ')) {
|
||||
item.warnings.push(`This ${type} starts with a space.`)
|
||||
if (eventOrProp.name?.startsWith(' ')) {
|
||||
record.warnings.push(`This ${type} starts with a space.`)
|
||||
}
|
||||
return item
|
||||
return record
|
||||
}
|
||||
) || []
|
||||
)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Input.Search
|
||||
@ -134,9 +144,9 @@ export function VolumeTable({ type, data }: { type: EventTableType; data: EventO
|
||||
<br />
|
||||
<br />
|
||||
<Table
|
||||
dataSource={searchTerm ? searchEvents(dataWithWarnings, searchTerm, type) : dataWithWarnings}
|
||||
dataSource={searchTerm ? search(dataWithWarnings, searchTerm) : dataWithWarnings}
|
||||
columns={columns}
|
||||
rowKey={type}
|
||||
rowKey={(item) => item.eventOrProp.name}
|
||||
size="small"
|
||||
style={{ marginBottom: '4rem' }}
|
||||
pagination={{ pageSize: 99999, hideOnSinglePage: true }}
|
||||
@ -165,15 +175,15 @@ export function UsageDisabledWarning({ tab }: { tab: string }): JSX.Element {
|
||||
}
|
||||
|
||||
export function EventsVolumeTable(): JSX.Element | null {
|
||||
const { currentTeam } = useValues(teamLogic)
|
||||
const { preflight } = useValues(preflightLogic)
|
||||
const { eventDefinitions, loaded } = useValues(eventDefinitionsLogic)
|
||||
|
||||
return currentTeam?.event_names_with_usage ? (
|
||||
return loaded ? (
|
||||
<>
|
||||
{preflight && !preflight?.is_event_property_usage_enabled ? (
|
||||
<UsageDisabledWarning tab="Events Stats" />
|
||||
) : (
|
||||
currentTeam?.event_names_with_usage[0]?.volume === null && (
|
||||
eventDefinitions[0].volume_30_day === null && (
|
||||
<>
|
||||
<Alert
|
||||
type="warning"
|
||||
@ -182,7 +192,9 @@ export function EventsVolumeTable(): JSX.Element | null {
|
||||
</>
|
||||
)
|
||||
)}
|
||||
<VolumeTable data={currentTeam?.event_names_with_usage as EventOrPropType[]} type="event" />
|
||||
<VolumeTable data={eventDefinitions} type="event" />
|
||||
</>
|
||||
) : null
|
||||
) : (
|
||||
<Skeleton active paragraph={{ rows: 5 }} />
|
||||
)
|
||||
}
|
||||
|
@ -1,28 +1,31 @@
|
||||
import React from 'react'
|
||||
import { useValues } from 'kea'
|
||||
import { VolumeTable, UsageDisabledWarning, EventOrPropType } from './EventsVolumeTable'
|
||||
import { Alert } from 'antd'
|
||||
import { VolumeTable, UsageDisabledWarning } from './EventsVolumeTable'
|
||||
import { Alert, Skeleton } from 'antd'
|
||||
import { preflightLogic } from 'scenes/PreflightCheck/logic'
|
||||
import { teamLogic } from 'scenes/teamLogic'
|
||||
import { propertyDefinitionsLogic } from './propertyDefinitionsLogic'
|
||||
|
||||
export function PropertiesVolumeTable(): JSX.Element | null {
|
||||
const { currentTeam } = useValues(teamLogic)
|
||||
const { preflight } = useValues(preflightLogic)
|
||||
return currentTeam?.event_properties_with_usage ? (
|
||||
const { propertyDefinitions, loaded } = useValues(propertyDefinitionsLogic)
|
||||
|
||||
return loaded ? (
|
||||
<>
|
||||
{preflight && !preflight?.is_event_property_usage_enabled ? (
|
||||
<UsageDisabledWarning tab="Properties Stats" />
|
||||
) : (
|
||||
currentTeam?.event_properties_with_usage[0]?.volume === null && (
|
||||
propertyDefinitions[0].volume_30_day === null && (
|
||||
<>
|
||||
<Alert
|
||||
type="warning"
|
||||
message="We haven't been able to get usage and volume data yet. Please check back later"
|
||||
message="We haven't been able to get usage and volume data yet. Please check back later."
|
||||
/>
|
||||
</>
|
||||
)
|
||||
)}
|
||||
<VolumeTable data={currentTeam?.event_properties_with_usage as EventOrPropType[]} type="property" />
|
||||
<VolumeTable data={propertyDefinitions} type="property" />
|
||||
</>
|
||||
) : null
|
||||
) : (
|
||||
<Skeleton active paragraph={{ rows: 5 }} />
|
||||
)
|
||||
}
|
||||
|
89
frontend/src/scenes/events/eventDefinitionsLogic.ts
Normal file
89
frontend/src/scenes/events/eventDefinitionsLogic.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import { kea } from 'kea'
|
||||
import api from 'lib/api'
|
||||
import { posthogEvents } from 'lib/utils'
|
||||
import { EventDefinition, SelectOption } from '~/types'
|
||||
import { eventDefinitionsLogicType } from './eventDefinitionsLogicType'
|
||||
|
||||
interface EventDefinitionStorage {
|
||||
count: number
|
||||
next: null | string
|
||||
results: EventDefinition[]
|
||||
}
|
||||
|
||||
interface EventsGroupedInterface {
|
||||
label: string
|
||||
options: SelectOption[]
|
||||
}
|
||||
|
||||
export const eventDefinitionsLogic = kea<
|
||||
eventDefinitionsLogicType<EventDefinitionStorage, EventDefinition, EventsGroupedInterface, SelectOption>
|
||||
>({
|
||||
loaders: ({ values }) => ({
|
||||
eventStorage: [
|
||||
{ results: [], next: null, count: 0 } as EventDefinitionStorage,
|
||||
{
|
||||
loadEventDefinitions: async (initial?: boolean) => {
|
||||
const url = initial
|
||||
? 'api/projects/@current/event_definitions/?limit=500'
|
||||
: values.eventStorage.next
|
||||
if (!url) {
|
||||
throw new Error('Incorrect call to eventDefinitionsLogic.loadEventDefinitions')
|
||||
}
|
||||
const eventStorage = await api.get(url)
|
||||
return {
|
||||
count: eventStorage.count,
|
||||
results: [...values.eventStorage.results, ...eventStorage.results],
|
||||
next: eventStorage.next,
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
listeners: ({ actions }) => ({
|
||||
loadEventDefinitionsSuccess: ({ eventStorage }) => {
|
||||
if (eventStorage.next) {
|
||||
actions.loadEventDefinitions()
|
||||
}
|
||||
},
|
||||
}),
|
||||
events: ({ actions }) => ({
|
||||
afterMount: () => {
|
||||
actions.loadEventDefinitions(true)
|
||||
},
|
||||
}),
|
||||
selectors: {
|
||||
loaded: [
|
||||
// Whether *all* the event definitions are fully loaded
|
||||
(s) => [s.eventStorage, s.eventStorageLoading],
|
||||
(eventStorage, eventStorageLoading): boolean => !eventStorageLoading && !eventStorage.next,
|
||||
],
|
||||
eventDefinitions: [(s) => [s.eventStorage], (eventStorage): EventDefinition[] => eventStorage.results],
|
||||
eventNames: [
|
||||
(s) => [s.eventDefinitions],
|
||||
(eventDefinitions): string[] => eventDefinitions.map((definition) => definition.name),
|
||||
],
|
||||
customEventNames: [
|
||||
(s) => [s.eventNames],
|
||||
(eventNames): string[] => eventNames.filter((event) => !event.startsWith('!')),
|
||||
],
|
||||
eventNamesGrouped: [
|
||||
(s) => [s.eventNames],
|
||||
(eventNames): EventsGroupedInterface[] => {
|
||||
const data: EventsGroupedInterface[] = [
|
||||
{ label: 'Custom events', options: [] },
|
||||
{ label: 'PostHog events', options: [] },
|
||||
]
|
||||
|
||||
eventNames.forEach((name: string) => {
|
||||
const format = { label: name, value: name }
|
||||
if (posthogEvents.includes(name)) {
|
||||
return data[1].options.push(format)
|
||||
}
|
||||
data[0].options.push(format)
|
||||
})
|
||||
|
||||
return data
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
82
frontend/src/scenes/events/propertyDefinitionsLogic.ts
Normal file
82
frontend/src/scenes/events/propertyDefinitionsLogic.ts
Normal file
@ -0,0 +1,82 @@
|
||||
import { kea } from 'kea'
|
||||
import api from 'lib/api'
|
||||
import { PropertyDefinition, SelectOption } from '~/types'
|
||||
import { propertyDefinitionsLogicType } from './propertyDefinitionsLogicType'
|
||||
|
||||
interface PropertySelectOption extends SelectOption {
|
||||
is_numerical?: boolean
|
||||
}
|
||||
|
||||
interface PropertyDefinitionStorage {
|
||||
count: number
|
||||
next: null | string
|
||||
results: PropertyDefinition[]
|
||||
}
|
||||
|
||||
export const propertyDefinitionsLogic = kea<
|
||||
propertyDefinitionsLogicType<PropertyDefinitionStorage, PropertyDefinition, PropertySelectOption>
|
||||
>({
|
||||
loaders: ({ values }) => ({
|
||||
propertyStorage: [
|
||||
{ results: [], next: null, count: 0 } as PropertyDefinitionStorage,
|
||||
{
|
||||
loadPropertyDefinitions: async (initial?: boolean) => {
|
||||
const url = initial
|
||||
? 'api/projects/@current/property_definitions/?limit=500'
|
||||
: values.propertyStorage.next
|
||||
if (!url) {
|
||||
throw new Error('Incorrect call to propertyDefinitionsLogic.loadPropertyDefinitions')
|
||||
}
|
||||
const propertyStorage = await api.get(url)
|
||||
return {
|
||||
count: propertyStorage.count,
|
||||
results: [...values.propertyStorage.results, ...propertyStorage.results],
|
||||
next: propertyStorage.next,
|
||||
}
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
listeners: ({ actions }) => ({
|
||||
loadPropertyDefinitionsSuccess: ({ propertyStorage }) => {
|
||||
if (propertyStorage.next) {
|
||||
actions.loadPropertyDefinitions()
|
||||
}
|
||||
},
|
||||
}),
|
||||
events: ({ actions }) => ({
|
||||
afterMount: () => {
|
||||
actions.loadPropertyDefinitions(true)
|
||||
},
|
||||
}),
|
||||
selectors: {
|
||||
loaded: [
|
||||
// Whether *all* the property definitions are fully loaded
|
||||
(s) => [s.propertyStorage, s.propertyStorageLoading],
|
||||
(propertyStorage, propertyStorageLoading): boolean => !propertyStorageLoading && !propertyStorage.next,
|
||||
],
|
||||
propertyDefinitions: [
|
||||
(s) => [s.propertyStorage],
|
||||
(propertyStorage): PropertyDefinition[] => propertyStorage.results || [],
|
||||
],
|
||||
transformedPropertyDefinitions: [
|
||||
// Transformed propertyDefinitions to use in `Select` components
|
||||
(s) => [s.propertyDefinitions],
|
||||
(propertyDefinitions): PropertySelectOption[] =>
|
||||
propertyDefinitions.map((property) => ({
|
||||
value: property.name,
|
||||
label: property.name,
|
||||
is_numerical: property.is_numerical,
|
||||
})),
|
||||
],
|
||||
propertyNames: [
|
||||
(s) => [s.propertyDefinitions],
|
||||
(propertyDefinitions): string[] => propertyDefinitions.map((definition) => definition.name),
|
||||
],
|
||||
numericalPropertyNames: [
|
||||
(s) => [s.transformedPropertyDefinitions],
|
||||
(transformedPropertyDefinitions): PropertySelectOption[] =>
|
||||
transformedPropertyDefinitions.filter((definition) => definition.is_numerical),
|
||||
],
|
||||
},
|
||||
})
|
@ -1,7 +1,6 @@
|
||||
import React, { RefObject } from 'react'
|
||||
import { BuiltLogic, useActions, useValues } from 'kea'
|
||||
import { ActionType } from '~/types'
|
||||
import { EventUsageType } from '~/types'
|
||||
import { ActionType, EventDefinition } from '~/types'
|
||||
import { EntityTypes } from '../../trends/trendsLogic'
|
||||
import { actionsModel } from '~/models/actionsModel'
|
||||
import { FireOutlined, InfoCircleOutlined, AimOutlined, ContainerOutlined } from '@ant-design/icons'
|
||||
@ -9,8 +8,8 @@ import { Tooltip } from 'antd'
|
||||
import { ActionSelectInfo } from '../ActionSelectInfo'
|
||||
import { SelectBox, SelectedItem } from '../../../lib/components/SelectBox'
|
||||
import { Link } from 'lib/components/Link'
|
||||
import { teamLogic } from 'scenes/teamLogic'
|
||||
import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo'
|
||||
import { eventDefinitionsLogic } from 'scenes/events/eventDefinitionsLogic'
|
||||
|
||||
interface FilterType {
|
||||
filter: {
|
||||
@ -26,10 +25,10 @@ interface FilterType {
|
||||
index: number
|
||||
}
|
||||
|
||||
const getSuggestions = (events: EventUsageType[]): EventUsageType[] => {
|
||||
const getSuggestions = (events: EventDefinition[]): EventDefinition[] => {
|
||||
return events
|
||||
.filter((event) => event.usage_count > 0)
|
||||
.sort((a, b) => b.usage_count - a.usage_count)
|
||||
.filter((event) => (event.query_usage_30_day || -1) > 0)
|
||||
.sort((a, b) => (b.query_usage_30_day || -1) - (a.query_usage_30_day || -1))
|
||||
.slice(0, 3)
|
||||
}
|
||||
|
||||
@ -52,7 +51,7 @@ export function ActionFilterDropdown({
|
||||
const { updateFilter, setEntityFilterVisibility } = useActions(logic)
|
||||
|
||||
const { actions } = useValues(actionsModel)
|
||||
const { currentTeam } = useValues(teamLogic)
|
||||
const { eventDefinitions } = useValues(eventDefinitionsLogic)
|
||||
|
||||
const handleDismiss = (event: MouseEvent): void => {
|
||||
if (openButtonRef?.current?.contains(event.target as Node)) {
|
||||
@ -68,7 +67,7 @@ export function ActionFilterDropdown({
|
||||
setEntityFilterVisibility(selectedFilter.index, true)
|
||||
}
|
||||
}
|
||||
const suggestions = getSuggestions(currentTeam?.event_names_with_usage || [])
|
||||
const suggestions = getSuggestions(eventDefinitions || [])
|
||||
|
||||
return (
|
||||
<SelectBox
|
||||
@ -85,10 +84,9 @@ export function ActionFilterDropdown({
|
||||
</Tooltip>
|
||||
</>
|
||||
),
|
||||
dataSource: suggestions.map((event) => ({
|
||||
key: 'suggestions' + event.event,
|
||||
name: event.event,
|
||||
...event,
|
||||
dataSource: suggestions.map((definition) => ({
|
||||
...definition,
|
||||
key: 'suggestions' + definition.id,
|
||||
})),
|
||||
renderInfo: function renderSuggestions({ item }) {
|
||||
return (
|
||||
@ -96,22 +94,22 @@ export function ActionFilterDropdown({
|
||||
<FireOutlined /> Suggestions
|
||||
<br />
|
||||
<h3>{item.name}</h3>
|
||||
{(item?.volume ?? 0 > 0) && (
|
||||
{(item?.volume_30_day ?? 0 > 0) && (
|
||||
<>
|
||||
Seen <strong>{item.volume}</strong> times.{' '}
|
||||
Seen <strong>{item.volume_30_day}</strong> times.{' '}
|
||||
</>
|
||||
)}
|
||||
{(item?.usage_count ?? 0 > 0) && (
|
||||
{(item?.query_usage_30_day ?? 0 > 0) && (
|
||||
<>
|
||||
Used in <strong>{item.usage_count}</strong> queries.
|
||||
Used in <strong>{item.query_usage_30_day}</strong> queries.
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
},
|
||||
type: EntityTypes.EVENTS,
|
||||
getValue: (item: SelectedItem) => item.event || '',
|
||||
getLabel: (item: SelectedItem) => item.event || '',
|
||||
getValue: (item: SelectedItem) => item.name || '',
|
||||
getLabel: (item: SelectedItem) => item.name || '',
|
||||
},
|
||||
{
|
||||
name: (
|
||||
@ -138,10 +136,9 @@ export function ActionFilterDropdown({
|
||||
</>
|
||||
),
|
||||
dataSource:
|
||||
currentTeam?.event_names_with_usage.map((event) => ({
|
||||
key: EntityTypes.EVENTS + event.event,
|
||||
name: event.event,
|
||||
...event,
|
||||
eventDefinitions.map((definition) => ({
|
||||
...definition,
|
||||
key: EntityTypes.EVENTS + definition.id,
|
||||
})) || [],
|
||||
renderInfo: function events({ item }) {
|
||||
return (
|
||||
@ -149,22 +146,22 @@ export function ActionFilterDropdown({
|
||||
<ContainerOutlined /> Events
|
||||
<br />
|
||||
<h3>{item.name}</h3>
|
||||
{(item?.volume ?? 0 > 0) && (
|
||||
{(item?.volume_30_day ?? 0 > 0) && (
|
||||
<>
|
||||
Seen <strong>{item.volume}</strong> times.{' '}
|
||||
Seen <strong>{item.volume_30_day}</strong> times.{' '}
|
||||
</>
|
||||
)}
|
||||
{(item?.usage_count ?? 0 > 0) && (
|
||||
{(item?.query_usage_30_day ?? 0 > 0) && (
|
||||
<>
|
||||
Used in <strong>{item.usage_count}</strong> queries.
|
||||
Used in <strong>{item.query_usage_30_day}</strong> queries.
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
},
|
||||
type: EntityTypes.EVENTS,
|
||||
getValue: (item: SelectedItem) => item.event || '',
|
||||
getLabel: (item: SelectedItem) => item.event || '',
|
||||
getValue: (item: SelectedItem) => item.name || '',
|
||||
getLabel: (item: SelectedItem) => item.name || '',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
@ -8,10 +8,10 @@ import { PROPERTY_MATH_TYPE, EVENT_MATH_TYPE, MATHS } from 'lib/constants'
|
||||
import { DownOutlined, DeleteOutlined } from '@ant-design/icons'
|
||||
import { SelectGradientOverflow } from 'lib/components/SelectGradientOverflow'
|
||||
import './ActionFilterRow.scss'
|
||||
import { teamLogic } from 'scenes/teamLogic'
|
||||
import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo'
|
||||
import { preflightLogic } from 'scenes/PreflightCheck/logic'
|
||||
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
|
||||
import { propertyDefinitionsLogic } from 'scenes/events/propertyDefinitionsLogic'
|
||||
|
||||
const EVENT_MATH_ENTRIES = Object.entries(MATHS).filter(([, item]) => item.type == EVENT_MATH_TYPE)
|
||||
const PROPERTY_MATH_ENTRIES = Object.entries(MATHS).filter(([, item]) => item.type == PROPERTY_MATH_TYPE)
|
||||
@ -47,7 +47,7 @@ export function ActionFilterRow({
|
||||
updateFilterProperty,
|
||||
setEntityFilterVisibility,
|
||||
} = useActions(logic)
|
||||
const { eventProperties, eventPropertiesNumerical } = useValues(teamLogic)
|
||||
const { propertyNames, numericalPropertyNames } = useValues(propertyDefinitionsLogic)
|
||||
|
||||
const visible = entityFilterVisible[filter.order]
|
||||
|
||||
@ -58,20 +58,20 @@ export function ActionFilterRow({
|
||||
const onClose = () => {
|
||||
removeLocalFilter({ value: filter.id, type: filter.type, index })
|
||||
}
|
||||
const onMathSelect = (_, math) => {
|
||||
const onMathSelect = (_, selectedMath) => {
|
||||
updateFilterMath({
|
||||
math,
|
||||
math_property: MATHS[math]?.onProperty ? mathProperty : undefined,
|
||||
onProperty: MATHS[math]?.onProperty,
|
||||
math: selectedMath,
|
||||
math_property: MATHS[selectedMath]?.onProperty ? mathProperty : undefined,
|
||||
onProperty: MATHS[selectedMath]?.onProperty,
|
||||
value: filter.id,
|
||||
type: filter.type,
|
||||
index: index,
|
||||
})
|
||||
}
|
||||
const onMathPropertySelect = (_, mathProperty) => {
|
||||
const onMathPropertySelect = (_, selectedMathProperty) => {
|
||||
updateFilterMath({
|
||||
math: filter.math,
|
||||
math_property: mathProperty,
|
||||
math_property: selectedMathProperty,
|
||||
value: filter.id,
|
||||
type: filter.type,
|
||||
index: index,
|
||||
@ -139,9 +139,7 @@ export function ActionFilterRow({
|
||||
math={math}
|
||||
index={index}
|
||||
onMathSelect={onMathSelect}
|
||||
areEventPropertiesNumericalAvailable={
|
||||
eventPropertiesNumerical && eventPropertiesNumerical.length > 0
|
||||
}
|
||||
areEventPropertiesNumericalAvailable={!!numericalPropertyNames.length}
|
||||
style={{ maxWidth: '100%', width: 'initial' }}
|
||||
/>
|
||||
)}
|
||||
@ -168,7 +166,7 @@ export function ActionFilterRow({
|
||||
mathProperty={mathProperty}
|
||||
index={index}
|
||||
onMathPropertySelect={onMathPropertySelect}
|
||||
properties={eventPropertiesNumerical}
|
||||
properties={numericalPropertyNames}
|
||||
/>
|
||||
)}
|
||||
{(!hidePropertySelector || (filter.properties && filter.properties.length > 0)) && (
|
||||
@ -188,7 +186,7 @@ export function ActionFilterRow({
|
||||
<div className="ml">
|
||||
<PropertyFilters
|
||||
pageKey={`${index}-${value}-filter`}
|
||||
properties={eventProperties}
|
||||
properties={propertyNames}
|
||||
propertyFilters={filter.properties}
|
||||
onChange={(properties) => updateFilterProperty({ properties, index })}
|
||||
style={{ marginBottom: 0 }}
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { kea } from 'kea'
|
||||
import { actionsModel } from '~/models/actionsModel'
|
||||
import { EntityTypes } from '../../trends/trendsLogic'
|
||||
import { teamLogic } from 'scenes/teamLogic'
|
||||
import { eventDefinitionsLogic } from 'scenes/events/eventDefinitionsLogic'
|
||||
|
||||
export function toLocalFilters(filters) {
|
||||
return [
|
||||
@ -33,7 +33,7 @@ export function toFilters(localFilters) {
|
||||
export const entityFilterLogic = kea({
|
||||
key: (props) => props.typeKey,
|
||||
connect: {
|
||||
values: [teamLogic, ['eventNames'], actionsModel, ['actions']],
|
||||
values: [actionsModel, ['actions']],
|
||||
},
|
||||
actions: () => ({
|
||||
selectFilter: (filter) => ({ filter }),
|
||||
@ -76,7 +76,7 @@ export const entityFilterLogic = kea({
|
||||
|
||||
selectors: ({ selectors }) => ({
|
||||
entities: [
|
||||
() => [selectors.eventNames, selectors.actions],
|
||||
() => [eventDefinitionsLogic.selectors.eventNames, selectors.actions],
|
||||
(events, actions) => {
|
||||
return {
|
||||
[EntityTypes.ACTIONS]: actions,
|
||||
|
@ -1,17 +1,17 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Tooltip, Select, Tabs, Popover, Button } from 'antd'
|
||||
import { useValues } from 'kea'
|
||||
import { teamLogic } from 'scenes/teamLogic'
|
||||
import { propertyFilterLogic } from 'lib/components/PropertyFilters/propertyFilterLogic'
|
||||
import { cohortsModel } from '../../models/cohortsModel'
|
||||
import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo'
|
||||
import { SelectGradientOverflow } from 'lib/components/SelectGradientOverflow'
|
||||
import { ShownAsValue } from 'lib/constants'
|
||||
import { propertyDefinitionsLogic } from 'scenes/events/propertyDefinitionsLogic'
|
||||
|
||||
const { TabPane } = Tabs
|
||||
|
||||
function PropertyFilter({ breakdown, onChange }) {
|
||||
const { eventProperties } = useValues(teamLogic)
|
||||
const { transformedPropertyDefinitions } = useValues(propertyDefinitionsLogic)
|
||||
const { personProperties } = useValues(propertyFilterLogic({ pageKey: 'breakdown' }))
|
||||
return (
|
||||
<SelectGradientOverflow
|
||||
@ -24,9 +24,9 @@ function PropertyFilter({ breakdown, onChange }) {
|
||||
filterOption={(input, option) => option.value?.toLowerCase().indexOf(input.toLowerCase()) >= 0}
|
||||
data-attr="prop-breakdown-select"
|
||||
>
|
||||
{eventProperties.length > 0 && (
|
||||
{transformedPropertyDefinitions.length > 0 && (
|
||||
<Select.OptGroup key="Event properties" label="Event properties">
|
||||
{Object.entries(eventProperties).map(([key, item], index) => (
|
||||
{Object.entries(transformedPropertyDefinitions).map(([key, item], index) => (
|
||||
<Select.Option
|
||||
key={'event_' + key}
|
||||
value={'event_' + item.value}
|
||||
@ -45,7 +45,7 @@ function PropertyFilter({ breakdown, onChange }) {
|
||||
key={'person_' + key}
|
||||
value={'person_' + item.value}
|
||||
type="person"
|
||||
data-attr={'prop-filter-person-' + (eventProperties.length + index)}
|
||||
data-attr={'prop-filter-person-' + (transformedPropertyDefinitions.length + index)}
|
||||
>
|
||||
<PropertyKeyInfo value={item.value} />
|
||||
</Select.Option>
|
||||
|
@ -12,10 +12,10 @@ import {
|
||||
import { Select } from 'antd'
|
||||
import { PropertyValue } from 'lib/components/PropertyFilters'
|
||||
import { TestAccountFilter } from '../TestAccountFilter'
|
||||
import { teamLogic } from 'scenes/teamLogic'
|
||||
import { eventDefinitionsLogic } from 'scenes/events/eventDefinitionsLogic'
|
||||
|
||||
export function PathTab(): JSX.Element {
|
||||
const { customEventNames } = useValues(teamLogic)
|
||||
const { customEventNames } = useValues(eventDefinitionsLogic)
|
||||
const { filter } = useValues(pathsLogic({ dashboardItemId: null }))
|
||||
const { setFilter } = useActions(pathsLogic({ dashboardItemId: null }))
|
||||
|
||||
|
@ -5,7 +5,7 @@ import { EventTypePropertyFilter } from '~/types'
|
||||
import { keyMapping } from 'lib/components/PropertyKeyInfo'
|
||||
import { OperatorValueSelect } from 'lib/components/PropertyFilters/OperatorValueSelect'
|
||||
import { sessionsFiltersLogic } from 'scenes/sessions/filters/sessionsFiltersLogic'
|
||||
import { teamLogic } from 'scenes/teamLogic'
|
||||
import { propertyDefinitionsLogic } from 'scenes/events/propertyDefinitionsLogic'
|
||||
|
||||
interface Props {
|
||||
filter: EventTypePropertyFilter
|
||||
@ -13,7 +13,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export function EventPropertyFilter({ filter, selector }: Props): JSX.Element {
|
||||
const { eventProperties } = useValues(teamLogic)
|
||||
const { transformedPropertyDefinitions } = useValues(propertyDefinitionsLogic)
|
||||
const { updateFilter } = useActions(sessionsFiltersLogic)
|
||||
|
||||
const property = filter.properties && filter.properties.length > 0 ? filter.properties[0] : null
|
||||
@ -29,7 +29,7 @@ export function EventPropertyFilter({ filter, selector }: Props): JSX.Element {
|
||||
{
|
||||
type: 'event',
|
||||
label: 'Event properties',
|
||||
options: eventProperties,
|
||||
options: transformedPropertyDefinitions,
|
||||
},
|
||||
]}
|
||||
onChange={(_, key) => {
|
||||
|
@ -9,14 +9,14 @@ import { ActionInfo } from 'scenes/insights/ActionFilter/ActionFilterDropdown'
|
||||
import { FilterSelector, sessionsFiltersLogic } from 'scenes/sessions/filters/sessionsFiltersLogic'
|
||||
import { Link } from 'lib/components/Link'
|
||||
import { cohortsModel } from '~/models/cohortsModel'
|
||||
import { teamLogic } from 'scenes/teamLogic'
|
||||
import { eventDefinitionsLogic } from 'scenes/events/eventDefinitionsLogic'
|
||||
|
||||
export function SessionsFilterBox({ selector }: { selector: FilterSelector }): JSX.Element | null {
|
||||
const { openFilter, personProperties } = useValues(sessionsFiltersLogic)
|
||||
|
||||
const { closeFilterSelect, dropdownSelected } = useActions(sessionsFiltersLogic)
|
||||
|
||||
const { currentTeam } = useValues(teamLogic)
|
||||
const { eventDefinitions } = useValues(eventDefinitionsLogic)
|
||||
const { actions } = useValues(actionsModel)
|
||||
const { cohorts } = useValues(cohortsModel)
|
||||
|
||||
@ -40,8 +40,8 @@ export function SessionsFilterBox({ selector }: { selector: FilterSelector }): J
|
||||
})),
|
||||
renderInfo: ActionInfo,
|
||||
type: 'action_type',
|
||||
getValue: (item: SelectedItem) => item.action?.id,
|
||||
getLabel: (item: SelectedItem) => item.action?.name,
|
||||
getValue: (item: SelectedItem) => item.action?.id || '',
|
||||
getLabel: (item: SelectedItem) => item.action?.name || '',
|
||||
},
|
||||
{
|
||||
name: (
|
||||
@ -50,10 +50,9 @@ export function SessionsFilterBox({ selector }: { selector: FilterSelector }): J
|
||||
</>
|
||||
),
|
||||
dataSource:
|
||||
(currentTeam?.event_names_with_usage ?? []).map((event) => ({
|
||||
key: EntityTypes.EVENTS + event.event,
|
||||
name: event.event,
|
||||
...event,
|
||||
eventDefinitions.map((definition) => ({
|
||||
key: EntityTypes.EVENTS + definition.name,
|
||||
...definition,
|
||||
})) || [],
|
||||
renderInfo: function events({ item }) {
|
||||
return (
|
||||
@ -61,22 +60,22 @@ export function SessionsFilterBox({ selector }: { selector: FilterSelector }): J
|
||||
<ContainerOutlined /> Events
|
||||
<br />
|
||||
<h3>{item.name}</h3>
|
||||
{item?.volume && (
|
||||
{item?.volume_30_day && (
|
||||
<>
|
||||
Seen <strong>{item.volume}</strong> times.{' '}
|
||||
Seen <strong>{item.volume_30_day}</strong> times.{' '}
|
||||
</>
|
||||
)}
|
||||
{item?.usage_count && (
|
||||
{item?.query_usage_30_day && (
|
||||
<>
|
||||
Used in <strong>{item.usage_count}</strong> queries.
|
||||
Used in <strong>{item.query_usage_30_day}</strong> queries.
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
},
|
||||
type: 'event_type',
|
||||
getValue: (item: SelectedItem) => item.event,
|
||||
getLabel: (item: SelectedItem) => item.event,
|
||||
getValue: (item: SelectedItem) => item.name,
|
||||
getLabel: (item: SelectedItem) => item.name,
|
||||
},
|
||||
{
|
||||
name: (
|
||||
@ -86,7 +85,7 @@ export function SessionsFilterBox({ selector }: { selector: FilterSelector }): J
|
||||
),
|
||||
dataSource: cohorts.map((cohort: CohortType) => ({
|
||||
key: 'cohorts' + cohort.id,
|
||||
name: cohort.name,
|
||||
name: cohort.name || '',
|
||||
id: cohort.id,
|
||||
cohort,
|
||||
})),
|
||||
@ -113,7 +112,7 @@ export function SessionsFilterBox({ selector }: { selector: FilterSelector }): J
|
||||
)
|
||||
},
|
||||
type: 'cohort',
|
||||
getValue: (item: SelectedItem) => item.id,
|
||||
getValue: (item: SelectedItem) => item.id || '',
|
||||
getLabel: (item: SelectedItem) => item.name,
|
||||
},
|
||||
]
|
||||
@ -136,9 +135,9 @@ export function SessionsFilterBox({ selector }: { selector: FilterSelector }): J
|
||||
<UsergroupAddOutlined /> User property
|
||||
<br />
|
||||
<h3>{item.name}</h3>
|
||||
{item?.usage_count && (
|
||||
{item?.query_usage_30_day && (
|
||||
<>
|
||||
<strong>{item.usage_count}</strong> users have this property.
|
||||
<strong>{item.query_usage_30_day}</strong> users have this property.
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
@ -170,7 +169,7 @@ export function SessionsFilterBox({ selector }: { selector: FilterSelector }): J
|
||||
)
|
||||
},
|
||||
type: 'recording',
|
||||
getValue: (item: SelectedItem) => item.value,
|
||||
getValue: (item: SelectedItem) => item.value || '',
|
||||
getLabel: (item: SelectedItem) => item.name,
|
||||
})
|
||||
|
||||
|
@ -5,14 +5,9 @@ import { TeamType } from '~/types'
|
||||
import { userLogic } from './userLogic'
|
||||
import { toast } from 'react-toastify'
|
||||
import React from 'react'
|
||||
import { posthogEvents, identifierToHuman, resolveWebhookService } from 'lib/utils'
|
||||
import { identifierToHuman, resolveWebhookService } from 'lib/utils'
|
||||
|
||||
export interface EventProperty {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export const teamLogic = kea<teamLogicType<TeamType, EventProperty>>({
|
||||
export const teamLogic = kea<teamLogicType<TeamType>>({
|
||||
actions: {
|
||||
deleteTeam: (team: TeamType) => ({ team }),
|
||||
deleteTeamSuccess: true,
|
||||
@ -97,52 +92,6 @@ export const teamLogic = kea<teamLogicType<TeamType, EventProperty>>({
|
||||
window.location.href = '/ingestion'
|
||||
},
|
||||
}),
|
||||
selectors: {
|
||||
eventProperties: [
|
||||
(s) => [s.currentTeam],
|
||||
(team): EventProperty[] =>
|
||||
team
|
||||
? team.event_properties.map(
|
||||
(property: string) => ({ value: property, label: property } as EventProperty)
|
||||
)
|
||||
: [],
|
||||
],
|
||||
eventPropertiesNumerical: [
|
||||
(s) => [s.currentTeam],
|
||||
(team): EventProperty[] =>
|
||||
team
|
||||
? team.event_properties_numerical.map(
|
||||
(property: string) => ({ value: property, label: property } as EventProperty)
|
||||
)
|
||||
: [],
|
||||
],
|
||||
eventNames: [(s) => [s.currentTeam], (team): string[] => team?.event_names ?? []],
|
||||
customEventNames: [
|
||||
(s) => [s.eventNames],
|
||||
(eventNames): string[] => {
|
||||
return eventNames.filter((event) => !event.startsWith('!'))
|
||||
},
|
||||
],
|
||||
eventNamesGrouped: [
|
||||
(s) => [s.currentTeam],
|
||||
(team) => {
|
||||
const data = [
|
||||
{ label: 'Custom events', options: [] as EventProperty[] },
|
||||
{ label: 'PostHog events', options: [] as EventProperty[] },
|
||||
]
|
||||
if (team) {
|
||||
team.event_names.forEach((name: string) => {
|
||||
const format = { label: name, value: name } as EventProperty
|
||||
if (posthogEvents.includes(name)) {
|
||||
return data[1].options.push(format)
|
||||
}
|
||||
data[0].options.push(format)
|
||||
})
|
||||
}
|
||||
return data
|
||||
},
|
||||
],
|
||||
},
|
||||
events: ({ actions }) => ({
|
||||
afterMount: [actions.loadCurrentTeam],
|
||||
}),
|
||||
|
@ -21,7 +21,8 @@ import { ActionType, EntityType, FilterType, PersonType, PropertyFilter, TrendRe
|
||||
import { cohortLogic } from 'scenes/persons/cohortLogic'
|
||||
import { trendsLogicType } from './trendsLogicType'
|
||||
import { dashboardItemsModel } from '~/models/dashboardItemsModel'
|
||||
import { teamLogic } from 'scenes/teamLogic'
|
||||
import { eventDefinitionsLogic } from 'scenes/events/eventDefinitionsLogic'
|
||||
import { propertyDefinitionsLogic } from 'scenes/events/propertyDefinitionsLogic'
|
||||
|
||||
interface TrendResponse {
|
||||
result: TrendResult[]
|
||||
@ -167,7 +168,7 @@ export const trendsLogic = kea<
|
||||
},
|
||||
|
||||
connect: {
|
||||
values: [teamLogic, ['eventNames'], actionsModel, ['actions']],
|
||||
values: [actionsModel, ['actions']],
|
||||
},
|
||||
|
||||
loaders: ({ values, props }) => ({
|
||||
@ -248,12 +249,11 @@ export const trendsLogic = kea<
|
||||
FilterType
|
||||
>,
|
||||
{
|
||||
setFilters: (state, { filters, mergeFilters }) => {
|
||||
return cleanFilters({
|
||||
setFilters: (state, { filters, mergeFilters }) =>
|
||||
cleanFilters({
|
||||
...(mergeFilters ? state : {}),
|
||||
...filters,
|
||||
})
|
||||
},
|
||||
}),
|
||||
},
|
||||
],
|
||||
people: [
|
||||
@ -317,8 +317,8 @@ export const trendsLogic = kea<
|
||||
|
||||
selectors: () => ({
|
||||
filtersLoading: [
|
||||
() => [teamLogic.selectors.currentTeamLoading],
|
||||
(currentTeamLoading: any) => currentTeamLoading,
|
||||
() => [eventDefinitionsLogic.selectors.loaded, propertyDefinitionsLogic.selectors.loaded],
|
||||
(eventsLoaded, propertiesLoaded) => !eventsLoaded || !propertiesLoaded,
|
||||
],
|
||||
results: [(selectors) => [selectors._results], (response) => response.result],
|
||||
resultsLoading: [(selectors) => [selectors._resultsLoading], (_resultsLoading) => _resultsLoading],
|
||||
@ -511,8 +511,8 @@ export const trendsLogic = kea<
|
||||
})
|
||||
actions.setBreakdownValuesLoading(false)
|
||||
},
|
||||
[teamLogic.actionTypes.loadCurrentTeamSuccess]: async () => {
|
||||
actions.setFilters(getDefaultFilters(values.filters, values.eventNames), true)
|
||||
[eventDefinitionsLogic.actionTypes.loadEventDefinitionsSuccess]: async () => {
|
||||
actions.setFilters(getDefaultFilters(values.filters, eventDefinitionsLogic.values.eventNames), true)
|
||||
},
|
||||
}),
|
||||
|
||||
@ -569,7 +569,10 @@ export const trendsLogic = kea<
|
||||
cleanSearchParams['compare'] = false
|
||||
}
|
||||
|
||||
Object.assign(cleanSearchParams, getDefaultFilters(cleanSearchParams, values.eventNames))
|
||||
Object.assign(
|
||||
cleanSearchParams,
|
||||
getDefaultFilters(cleanSearchParams, eventDefinitionsLogic.values.eventNames)
|
||||
)
|
||||
|
||||
if (!objectsEqual(cleanSearchParams, values.filters)) {
|
||||
actions.setFilters(cleanSearchParams, false)
|
||||
|
@ -120,11 +120,6 @@ export interface TeamBasicType {
|
||||
export interface TeamType extends TeamBasicType {
|
||||
anonymize_ips: boolean
|
||||
app_urls: string[]
|
||||
event_names: string[]
|
||||
event_properties: string[]
|
||||
event_properties_numerical: string[]
|
||||
event_names_with_usage: EventUsageType[]
|
||||
event_properties_with_usage: PropertyUsageType[]
|
||||
slack_incoming_webhook: string
|
||||
session_recording_opt_in: boolean
|
||||
session_recording_retention_period_days: number | null
|
||||
@ -675,3 +670,30 @@ export interface LicenseType {
|
||||
max_users: string | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface EventDefinition {
|
||||
id: string
|
||||
name: string
|
||||
volume_30_day: number | null
|
||||
query_usage_30_day: number | null
|
||||
}
|
||||
|
||||
export interface PropertyDefinition {
|
||||
id: string
|
||||
name: string
|
||||
volume_30_day: number | null
|
||||
query_usage_30_day: number | null
|
||||
is_numerical?: boolean // Marked as optional to allow merge of EventDefinition & PropertyDefinition
|
||||
}
|
||||
|
||||
export interface SelectOption {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
export interface KeyMapping {
|
||||
label: string
|
||||
description: string | JSX.Element
|
||||
examples?: string[]
|
||||
hide?: boolean
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
from typing import Any, Dict, List, Optional, Type, cast
|
||||
from typing import Any, Dict, Optional, Type, cast
|
||||
|
||||
from django.db import transaction
|
||||
from django.shortcuts import get_object_or_404
|
||||
@ -10,12 +10,7 @@ from posthog.mixins import AnalyticsDestroyModelMixin
|
||||
from posthog.models import Organization, Team
|
||||
from posthog.models.user import User
|
||||
from posthog.models.utils import generate_random_token
|
||||
from posthog.permissions import (
|
||||
CREATE_METHODS,
|
||||
OrganizationAdminWritePermissions,
|
||||
OrganizationMemberPermissions,
|
||||
ProjectMembershipNecessaryPermissions,
|
||||
)
|
||||
from posthog.permissions import CREATE_METHODS, OrganizationAdminWritePermissions, ProjectMembershipNecessaryPermissions
|
||||
|
||||
|
||||
class PremiumMultiprojectPermissions(permissions.BasePermission):
|
||||
@ -37,10 +32,6 @@ class PremiumMultiprojectPermissions(permissions.BasePermission):
|
||||
|
||||
|
||||
class TeamSerializer(serializers.ModelSerializer):
|
||||
|
||||
event_names_with_usage = serializers.SerializerMethodField()
|
||||
event_properties_with_usage = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Team
|
||||
fields = (
|
||||
@ -62,11 +53,6 @@ class TeamSerializer(serializers.ModelSerializer):
|
||||
"data_attributes",
|
||||
"session_recording_opt_in",
|
||||
"session_recording_retention_period_days",
|
||||
"event_names",
|
||||
"event_properties",
|
||||
"event_properties_numerical",
|
||||
"event_names_with_usage",
|
||||
"event_properties_with_usage",
|
||||
)
|
||||
read_only_fields = (
|
||||
"id",
|
||||
@ -77,9 +63,6 @@ class TeamSerializer(serializers.ModelSerializer):
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"ingested_event",
|
||||
"event_names",
|
||||
"event_properties",
|
||||
"event_properties_numerical",
|
||||
)
|
||||
|
||||
def create(self, validated_data: Dict[str, Any], **kwargs) -> Team:
|
||||
@ -92,12 +75,6 @@ class TeamSerializer(serializers.ModelSerializer):
|
||||
request.user.save()
|
||||
return team
|
||||
|
||||
def get_event_names_with_usage(self, instance: Team) -> List:
|
||||
return instance.get_latest_event_names_with_usage()
|
||||
|
||||
def get_event_properties_with_usage(self, instance: Team) -> List:
|
||||
return instance.get_latest_event_properties_with_usage()
|
||||
|
||||
|
||||
class TeamViewSet(AnalyticsDestroyModelMixin, viewsets.ModelViewSet):
|
||||
serializer_class = TeamSerializer
|
||||
|
@ -17,6 +17,8 @@ class TestTeamAPI(APIBaseTest):
|
||||
self.assertEqual(response_data["results"][0]["name"], self.team.name)
|
||||
self.assertNotIn("test_account_filters", response_data["results"][0])
|
||||
self.assertNotIn("data_attributes", response_data["results"][0])
|
||||
|
||||
# TODO: #4070 These assertions will no longer make sense when we fully remove these attributes from the model
|
||||
self.assertNotIn("event_names", response_data["results"][0])
|
||||
self.assertNotIn("event_properties", response_data["results"][0])
|
||||
self.assertNotIn("event_properties_numerical", response_data["results"][0])
|
||||
@ -31,11 +33,13 @@ class TestTeamAPI(APIBaseTest):
|
||||
self.assertEqual(response_data["timezone"], "UTC")
|
||||
self.assertEqual(response_data["is_demo"], False)
|
||||
self.assertEqual(response_data["slack_incoming_webhook"], self.team.slack_incoming_webhook)
|
||||
self.assertIn("event_names", response_data)
|
||||
self.assertIn("event_properties", response_data)
|
||||
self.assertIn("event_properties_numerical", response_data)
|
||||
self.assertIn("event_names_with_usage", response_data)
|
||||
self.assertIn("event_properties_with_usage", response_data)
|
||||
# The properties below are no longer included as part of the request
|
||||
# TODO: #4070 These assertions will no longer make sense when we fully remove these attributes from the model
|
||||
self.assertNotIn("event_names", response_data)
|
||||
self.assertNotIn("event_properties", response_data)
|
||||
self.assertNotIn("event_properties_numerical", response_data)
|
||||
self.assertNotIn("event_names_with_usage", response_data)
|
||||
self.assertNotIn("event_properties_with_usage", response_data)
|
||||
|
||||
def test_cant_retrieve_project_from_another_org(self):
|
||||
org = Organization.objects.create(name="New Org")
|
||||
|
@ -32,8 +32,8 @@ def sync_event_and_properties_definitions(team_uuid: str) -> None:
|
||||
# Add or update any existing events
|
||||
for event in team.event_names:
|
||||
instance, _ = EventDefinition.objects.get_or_create(team=team, name=event)
|
||||
instance.volume_30_day = transformed_event_usage.get(event, {}).get("volume") or 0
|
||||
instance.query_usage_30_day = transformed_event_usage.get(event, {}).get("usage_count") or 0
|
||||
instance.volume_30_day = transformed_event_usage.get(event, {}).get("volume")
|
||||
instance.query_usage_30_day = transformed_event_usage.get(event, {}).get("usage_count")
|
||||
instance.save()
|
||||
|
||||
# Remove any deleted events
|
||||
@ -42,8 +42,8 @@ def sync_event_and_properties_definitions(team_uuid: str) -> None:
|
||||
# Add or update any existing properties
|
||||
for property in team.event_properties:
|
||||
property_instance, _ = PropertyDefinition.objects.get_or_create(team=team, name=property)
|
||||
property_instance.volume_30_day = transformed_property_usage.get(property, {}).get("volume") or 0
|
||||
property_instance.query_usage_30_day = transformed_property_usage.get(property, {}).get("usage_count") or 0
|
||||
property_instance.volume_30_day = transformed_property_usage.get(property, {}).get("volume")
|
||||
property_instance.query_usage_30_day = transformed_property_usage.get(property, {}).get("usage_count")
|
||||
property_instance.is_numerical = property in team.event_properties_numerical
|
||||
property_instance.save()
|
||||
|
||||
|
@ -56,8 +56,8 @@ class TestTeam(BaseTest):
|
||||
|
||||
for obj in EventDefinition.objects.filter(team=team):
|
||||
self.assertIn(obj.name, expected_events)
|
||||
self.assertEqual(obj.volume_30_day, 0)
|
||||
self.assertEqual(obj.query_usage_30_day, 0)
|
||||
self.assertEqual(obj.volume_30_day, None)
|
||||
self.assertEqual(obj.query_usage_30_day, None)
|
||||
|
||||
# Test adding and removing one event
|
||||
team.event_names.pop(0)
|
||||
@ -111,8 +111,8 @@ class TestTeam(BaseTest):
|
||||
|
||||
for obj in PropertyDefinition.objects.filter(team=team):
|
||||
self.assertIn(obj.name, expected_properties)
|
||||
self.assertEqual(obj.volume_30_day, 0)
|
||||
self.assertEqual(obj.query_usage_30_day, 0)
|
||||
self.assertEqual(obj.volume_30_day, None)
|
||||
self.assertEqual(obj.query_usage_30_day, None)
|
||||
self.assertEqual(obj.is_numerical, obj.name in numerical_properties)
|
||||
|
||||
# Test adding and removing one event
|
||||
|
Loading…
Reference in New Issue
Block a user