mirror of
https://github.com/PostHog/posthog.git
synced 2024-11-24 09:14:46 +01:00
parent
c7b71c4e53
commit
ebddd25ba5
@ -224,10 +224,10 @@ export function UnifiedPropertyFilter({ index, onComplete, logic }: PropertyFilt
|
||||
onClick={onClick}
|
||||
style={{ display: 'flex', alignItems: 'center' }}
|
||||
ref={selectBoxToggleRef}
|
||||
icon={!key ? <PlusOutlined /> : null}
|
||||
>
|
||||
{!key && <PlusOutlined style={{ fontSize: 10 }} />}
|
||||
<span className="text-overflow" style={{ maxWidth: '100%' }}>
|
||||
<PropertyKeyInfo value={key || 'Add filter'} />
|
||||
{key ? <PropertyKeyInfo value={key} /> : 'Add filter'}
|
||||
</span>
|
||||
{key && <DownOutlined style={{ fontSize: 10 }} />}
|
||||
</Button>
|
||||
|
@ -256,9 +256,9 @@ export function IconToolbar(): JSX.Element {
|
||||
)
|
||||
}
|
||||
|
||||
export function IconExternalLink(): JSX.Element {
|
||||
export function IconExternalLink({ style }: { style?: CSSProperties }): JSX.Element {
|
||||
return (
|
||||
<svg width="1em" height="1em" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg width="1em" height="1em" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg" style={style}>
|
||||
<g clipPath="url(#clip0)">
|
||||
<path
|
||||
d="M18.8614 14.5629C19.0932 14.8002 19.4101 14.9387 19.7441 14.9486C20.0782 14.9586 20.4029 14.8393 20.6489 14.6163L28.6771 7.33958C28.7082 7.31062 28.7494 7.29449 28.7922 7.29449C28.835 7.29449 28.8761 7.31062 28.9073 7.33958L30.2885 8.64768C30.3167 8.67449 30.3516 8.69343 30.3897 8.70254C30.4278 8.71165 30.4676 8.71062 30.5052 8.69954C30.5427 8.68846 30.5766 8.66774 30.6033 8.6395C30.63 8.61126 30.6487 8.57652 30.6573 8.53884C31.1031 6.61878 31.5493 4.69941 31.9958 2.78072C32.0025 2.7516 32.0019 2.72132 31.9941 2.69246C31.9863 2.66361 31.9715 2.63705 31.9511 2.61505C31.9306 2.59305 31.905 2.57627 31.8765 2.56614C31.848 2.55601 31.8175 2.55283 31.7875 2.55689L25.6781 3.48714C25.6461 3.49196 25.6161 3.5055 25.5914 3.52623C25.5668 3.54697 25.5485 3.57407 25.5386 3.60448C25.5288 3.6349 25.5278 3.66742 25.5356 3.69839C25.5435 3.72936 25.56 3.75754 25.5833 3.77976L26.9645 5.09608C26.981 5.11161 26.9941 5.13026 27.003 5.15091C27.012 5.17155 27.0166 5.19377 27.0166 5.21622C27.0166 5.23867 27.012 5.26088 27.003 5.28153C26.9941 5.30217 26.981 5.32082 26.9645 5.33635L18.9135 12.7681C18.7895 12.882 18.6896 13.019 18.6198 13.1712C18.5499 13.3234 18.5115 13.4878 18.5066 13.6548C18.5018 13.8218 18.5306 13.988 18.5916 14.1439C18.6525 14.2998 18.7442 14.4422 18.8614 14.5629Z"
|
||||
|
@ -43,6 +43,7 @@ export const annotationScopeToName = new Map<string, string>([
|
||||
|
||||
export const PERSON_DISTINCT_ID_MAX_SIZE = 3
|
||||
|
||||
// Event constants
|
||||
export const PAGEVIEW = '$pageview'
|
||||
export const AUTOCAPTURE = '$autocapture'
|
||||
export const SCREEN = '$screen'
|
||||
@ -58,9 +59,13 @@ export enum ShownAsValue {
|
||||
LIFECYCLE = 'Lifecycle',
|
||||
}
|
||||
|
||||
// Retention constants
|
||||
export const RETENTION_RECURRING = 'retention_recurring'
|
||||
export const RETENTION_FIRST_TIME = 'retention_first_time'
|
||||
|
||||
// Properties constants
|
||||
export const PROPERTY_MATH_TYPE = 'property'
|
||||
export const EVENT_MATH_TYPE = 'event'
|
||||
|
||||
export const MATHS: Record<string, any> = {
|
||||
total: {
|
||||
name: 'Total volume',
|
||||
|
@ -93,6 +93,7 @@ export function ActionFilter({
|
||||
singleFilter={singleFilter}
|
||||
showOr={showOr}
|
||||
horizontalUI={horizontalUI}
|
||||
filterCount={localFilters.length}
|
||||
/>
|
||||
))
|
||||
)
|
||||
|
@ -69,6 +69,12 @@
|
||||
&.visible {
|
||||
background-color: $primary;
|
||||
color: #fff;
|
||||
|
||||
&:hover {
|
||||
background-color: #fff;
|
||||
color: $primary;
|
||||
border: 1px solid $primary;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -80,6 +86,17 @@
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
}
|
||||
|
||||
.horizontal-ui {
|
||||
.row-action-btn {
|
||||
&.delete {
|
||||
padding: 0;
|
||||
width: unset;
|
||||
color: rgba($danger, 0.6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-row-letter {
|
||||
line-height: 32px;
|
||||
padding-right: 1px;
|
||||
|
@ -5,7 +5,7 @@ import { ActionFilter, EntityTypes, PropertyFilter, SelectOption } from '~/types
|
||||
import { ActionFilterDropdown } from './ActionFilterDropdown'
|
||||
import { PropertyFilters } from 'lib/components/PropertyFilters/PropertyFilters'
|
||||
import { PROPERTY_MATH_TYPE, EVENT_MATH_TYPE, MATHS } from 'lib/constants'
|
||||
import { DownOutlined, DeleteOutlined, FilterOutlined } from '@ant-design/icons'
|
||||
import { DownOutlined, DeleteOutlined, FilterOutlined, CloseSquareOutlined } from '@ant-design/icons'
|
||||
import { SelectGradientOverflow } from 'lib/components/SelectGradientOverflow'
|
||||
import { BareEntity, entityFilterLogic } from '../entityFilterLogic'
|
||||
import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo'
|
||||
@ -38,6 +38,7 @@ interface ActionFilterRowProps {
|
||||
showOr?: boolean
|
||||
letter?: string | null
|
||||
horizontalUI?: boolean
|
||||
filterCount: number
|
||||
}
|
||||
|
||||
export function ActionFilterRow({
|
||||
@ -50,6 +51,7 @@ export function ActionFilterRow({
|
||||
showOr,
|
||||
letter,
|
||||
horizontalUI = false,
|
||||
filterCount,
|
||||
}: ActionFilterRowProps): JSX.Element {
|
||||
const node = useRef<HTMLElement>(null)
|
||||
const { selectedFilter, entities, entityFilterVisible } = useValues(logic)
|
||||
@ -109,53 +111,60 @@ export function ActionFilterRow({
|
||||
value = entity.id || filter.id
|
||||
}
|
||||
|
||||
const orLabel = <div className="stateful-badge mc-main or width-locked">OR</div>
|
||||
|
||||
return (
|
||||
<div className={horizontalUI ? 'action-row-striped' : ''}>
|
||||
{showOr && (
|
||||
<Row align="middle">
|
||||
{index > 0 && (
|
||||
<div className="stateful-badge mc-main or width-locked" style={{ marginTop: 12 }}>
|
||||
OR
|
||||
</div>
|
||||
)}
|
||||
{!horizontalUI && index > 0 && showOr && (
|
||||
<Row align="middle" style={{ marginTop: 12 }}>
|
||||
{orLabel}
|
||||
</Row>
|
||||
)}
|
||||
|
||||
<Row gutter={8} align="middle" className={!horizontalUI ? 'mt' : ''}>
|
||||
{horizontalUI && !singleFilter && filterCount > 1 && (
|
||||
<Col>
|
||||
<Button
|
||||
type="link"
|
||||
onClick={onClose}
|
||||
className="row-action-btn delete"
|
||||
title="Remove graph series"
|
||||
danger
|
||||
icon={<CloseSquareOutlined />}
|
||||
/>
|
||||
</Col>
|
||||
)}
|
||||
{letter && (
|
||||
<Col className="action-row-letter">
|
||||
<span>{letter}</span>
|
||||
</Col>
|
||||
)}
|
||||
{horizontalUI && (
|
||||
{horizontalUI && !hideMathSelector && (
|
||||
<>
|
||||
<Col>Showing</Col>
|
||||
{!hideMathSelector && (
|
||||
<Col style={{ maxWidth: `calc(50% - 16px${letter ? ' - 32px' : ''})` }}>
|
||||
<MathSelector
|
||||
math={math}
|
||||
index={index}
|
||||
onMathSelect={onMathSelect}
|
||||
areEventPropertiesNumericalAvailable={!!numericalPropertyNames.length}
|
||||
style={{ maxWidth: '100%', width: 'initial' }}
|
||||
/>
|
||||
</Col>
|
||||
{MATHS[math || '']?.onProperty && (
|
||||
<>
|
||||
<Col>of</Col>
|
||||
<Col style={{ maxWidth: `calc(50% - 16px${letter ? ' - 32px' : ''})` }}>
|
||||
<MathSelector
|
||||
<MathPropertySelector
|
||||
name={name}
|
||||
math={math}
|
||||
mathProperty={mathProperty}
|
||||
index={index}
|
||||
onMathSelect={onMathSelect}
|
||||
areEventPropertiesNumericalAvailable={!!numericalPropertyNames.length}
|
||||
style={{ maxWidth: '100%', width: 'initial' }}
|
||||
onMathPropertySelect={onMathPropertySelect}
|
||||
properties={numericalPropertyNames}
|
||||
horizontalUI={horizontalUI}
|
||||
/>
|
||||
</Col>
|
||||
{MATHS[math || '']?.onProperty && (
|
||||
<>
|
||||
<Col>of</Col>
|
||||
<Col style={{ maxWidth: `calc(50% - 16px${letter ? ' - 32px' : ''})` }}>
|
||||
<MathPropertySelector
|
||||
name={name}
|
||||
math={math}
|
||||
mathProperty={mathProperty}
|
||||
index={index}
|
||||
onMathPropertySelect={onMathPropertySelect}
|
||||
properties={numericalPropertyNames}
|
||||
horizontalUI={horizontalUI}
|
||||
/>
|
||||
</Col>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Col>{separatorWord}</Col>
|
||||
@ -202,7 +211,7 @@ export function ActionFilterRow({
|
||||
? setEntityFilterVisibility(filter.order, !visible)
|
||||
: undefined
|
||||
}}
|
||||
className={`row-action-btn show-filters ${visible ? 'visible' : ''}`}
|
||||
className={`row-action-btn show-filters${filter.properties?.length ? ' visible' : ''}`}
|
||||
data-attr={'show-prop-filter-' + index}
|
||||
title="Show filters"
|
||||
>
|
||||
@ -211,7 +220,7 @@ export function ActionFilterRow({
|
||||
</Button>
|
||||
</Col>
|
||||
)}
|
||||
{!singleFilter && (
|
||||
{!horizontalUI && !singleFilter && filterCount > 1 && (
|
||||
<Col>
|
||||
<Button
|
||||
type="link"
|
||||
@ -224,6 +233,7 @@ export function ActionFilterRow({
|
||||
</Button>
|
||||
</Col>
|
||||
)}
|
||||
{horizontalUI && filterCount > 1 && index < filterCount - 1 && showOr && orLabel}
|
||||
</Row>
|
||||
{!horizontalUI && !hideMathSelector && MATHS[math || '']?.onProperty && (
|
||||
<Row align="middle">
|
||||
|
@ -43,6 +43,7 @@ export const SortableActionFilterRow = sortableElement(
|
||||
key={filterIndex}
|
||||
hideMathSelector={hideMathSelector}
|
||||
hidePropertySelector={hidePropertySelector}
|
||||
filterCount={filterCount}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
@ -11,6 +11,7 @@ import { DisplayType, FilterType } from '~/types'
|
||||
import { ViewType } from '../insightLogic'
|
||||
import { CalendarOutlined } from '@ant-design/icons'
|
||||
import { InsightDateFilter } from '../InsightDateFilter'
|
||||
import { RetentionDatePicker } from '../RetentionDatePicker'
|
||||
|
||||
interface InsightDisplayConfigProps {
|
||||
clearAnnotationsToCreate: () => void
|
||||
@ -157,8 +158,10 @@ function HorizontalDefaultInsightDisplayConfig({
|
||||
disabled={allFilters.insight === ViewType.LIFECYCLE}
|
||||
/>
|
||||
)}
|
||||
|
||||
{showIntervalFilter(activeView, allFilters) && <IntervalFilter view={activeView} />}
|
||||
|
||||
{activeView === ViewType.RETENTION && <RetentionDatePicker />}
|
||||
|
||||
{showDateFilter[activeView] && (
|
||||
<>
|
||||
<InsightDateFilter
|
||||
|
@ -13,8 +13,15 @@ import { Select } from 'antd'
|
||||
import { PropertyValue } from 'lib/components/PropertyFilters'
|
||||
import { TestAccountFilter } from '../TestAccountFilter'
|
||||
import { eventDefinitionsLogic } from 'scenes/events/eventDefinitionsLogic'
|
||||
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
|
||||
import { PathTabHorizontal } from './PathTabHorizontal'
|
||||
|
||||
export function PathTab(): JSX.Element {
|
||||
const { featureFlags } = useValues(featureFlagLogic)
|
||||
return featureFlags['4050-query-ui-optB'] ? <PathTabHorizontal /> : <DefaultPathTab />
|
||||
}
|
||||
|
||||
function DefaultPathTab(): JSX.Element {
|
||||
const { customEventNames } = useValues(eventDefinitionsLogic)
|
||||
const { filter } = useValues(pathsLogic({ dashboardItemId: null }))
|
||||
const { setFilter } = useActions(pathsLogic({ dashboardItemId: null }))
|
||||
|
@ -0,0 +1,82 @@
|
||||
import React from 'react'
|
||||
import { useValues, useActions } from 'kea'
|
||||
import { PropertyFilters } from 'lib/components/PropertyFilters/PropertyFilters'
|
||||
import {
|
||||
PAGEVIEW,
|
||||
AUTOCAPTURE,
|
||||
CUSTOM_EVENT,
|
||||
pathOptionsToLabels,
|
||||
pathOptionsToProperty,
|
||||
pathsLogic,
|
||||
} from 'scenes/paths/pathsLogic'
|
||||
import { Col, Row, Select, Skeleton } from 'antd'
|
||||
import { PropertyValue } from 'lib/components/PropertyFilters'
|
||||
import { TestAccountFilter } from '../TestAccountFilter'
|
||||
import { eventDefinitionsLogic } from 'scenes/events/eventDefinitionsLogic'
|
||||
import useBreakpoint from 'antd/lib/grid/hooks/useBreakpoint'
|
||||
|
||||
export function PathTabHorizontal(): JSX.Element {
|
||||
const { customEventNames } = useValues(eventDefinitionsLogic)
|
||||
const { filter, filtersLoading } = useValues(pathsLogic({ dashboardItemId: null }))
|
||||
const { setFilter } = useActions(pathsLogic({ dashboardItemId: null }))
|
||||
|
||||
const screens = useBreakpoint()
|
||||
const isSmallScreen = screens.xs || (screens.sm && !screens.md)
|
||||
|
||||
return (
|
||||
<Row gutter={16}>
|
||||
<Col md={16} xs={24}>
|
||||
<Row gutter={8} align="middle" className="mt">
|
||||
<Col>Showing paths from</Col>
|
||||
<Col>
|
||||
<Select
|
||||
value={filter?.path_type || PAGEVIEW}
|
||||
defaultValue={PAGEVIEW}
|
||||
dropdownMatchSelectWidth={false}
|
||||
onChange={(value): void => setFilter({ path_type: value, start_point: null })}
|
||||
style={{ paddingTop: 2 }}
|
||||
>
|
||||
{Object.entries(pathOptionsToLabels).map(([value, name], index) => {
|
||||
return (
|
||||
<Select.Option key={index} value={value}>
|
||||
{name}
|
||||
</Select.Option>
|
||||
)
|
||||
})}
|
||||
</Select>
|
||||
</Col>
|
||||
<Col>starting at</Col>
|
||||
<Col>
|
||||
<PropertyValue
|
||||
endpoint={filter.path_type === AUTOCAPTURE && 'api/paths/elements'}
|
||||
outerOptions={
|
||||
filter.path_type === CUSTOM_EVENT &&
|
||||
customEventNames.map((name) => ({
|
||||
name,
|
||||
}))
|
||||
}
|
||||
onSet={(value: string | number): void => setFilter({ start_point: value })}
|
||||
propertyKey={pathOptionsToProperty[filter.path_type || PAGEVIEW]}
|
||||
type="event"
|
||||
style={{ width: 200, paddingTop: 2 }}
|
||||
value={filter.start_point}
|
||||
placeholder={'Select start element'}
|
||||
operator={null}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col md={8} xs={24} style={{ marginTop: isSmallScreen ? '2rem' : 0 }}>
|
||||
<h4 className="secondary">Global Filters</h4>
|
||||
{filtersLoading ? (
|
||||
<Skeleton active paragraph={{ rows: 1 }} />
|
||||
) : (
|
||||
<>
|
||||
<PropertyFilters pageKey="insight-path" />
|
||||
<TestAccountFilter filters={filter} onChange={setFilter} />
|
||||
</>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
@ -21,10 +21,17 @@ import { FilterType } from '~/types'
|
||||
import { TestAccountFilter } from '../TestAccountFilter'
|
||||
import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo'
|
||||
import './RetentionTab.scss'
|
||||
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
|
||||
import { RetentionTabHorizontal } from './RetentionTabHorizontal'
|
||||
|
||||
const DatePicker = generatePicker<dayjs.Dayjs>(dayjsGenerateConfig)
|
||||
|
||||
export function RetentionTab(): JSX.Element {
|
||||
const { featureFlags } = useValues(featureFlagLogic)
|
||||
return featureFlags['4050-query-ui-optB'] ? <RetentionTabHorizontal /> : <DefaultRetentionTab />
|
||||
}
|
||||
|
||||
function DefaultRetentionTab(): JSX.Element {
|
||||
const node = useRef<HTMLElement>(null)
|
||||
const returningNode = useRef<HTMLElement>(null)
|
||||
const [open, setOpen] = useState<boolean>(false)
|
||||
|
@ -0,0 +1,187 @@
|
||||
import React, { useState, useRef } from 'react'
|
||||
import { useValues, useActions } from 'kea'
|
||||
import { PropertyFilters } from 'lib/components/PropertyFilters/PropertyFilters'
|
||||
import { ActionFilterDropdown } from '../ActionFilter/ActionFilterRow/ActionFilterDropdown'
|
||||
import { entityFilterLogic } from '../ActionFilter/entityFilterLogic'
|
||||
|
||||
import { DownOutlined, InfoCircleOutlined } from '@ant-design/icons'
|
||||
import { retentionTableLogic, dateOptions, retentionOptionDescriptions } from 'scenes/retention/retentionTableLogic'
|
||||
import { Button, Select, Tooltip, Row, Col, Skeleton } from 'antd'
|
||||
|
||||
import { FilterType, RetentionType } from '~/types'
|
||||
import { TestAccountFilter } from '../TestAccountFilter'
|
||||
import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo'
|
||||
import './RetentionTab.scss'
|
||||
import { RETENTION_FIRST_TIME, RETENTION_RECURRING } from 'lib/constants'
|
||||
import useBreakpoint from 'antd/lib/grid/hooks/useBreakpoint'
|
||||
import { IconExternalLink } from 'lib/components/icons'
|
||||
|
||||
export function RetentionTabHorizontal(): JSX.Element {
|
||||
const node = useRef<HTMLElement>(null)
|
||||
const returningNode = useRef<HTMLElement>(null)
|
||||
const [open, setOpen] = useState<boolean>(false)
|
||||
const [returningOpen, setReturningOpen] = useState<boolean>(false)
|
||||
const { filters, actionsLookup, filtersLoading } = useValues(retentionTableLogic({ dashboardItemId: null }))
|
||||
const { setFilters } = useActions(retentionTableLogic({ dashboardItemId: null }))
|
||||
|
||||
const screens = useBreakpoint()
|
||||
const isSmallScreen = screens.xs || (screens.sm && !screens.md)
|
||||
|
||||
const entityLogic = entityFilterLogic({
|
||||
setFilters: (filters: FilterType) => {
|
||||
if (filters.events && filters.events.length > 0) {
|
||||
setFilters({ target_entity: filters.events[0] })
|
||||
} else if (filters.actions && filters.actions.length > 0) {
|
||||
setFilters({ target_entity: filters.actions[0] })
|
||||
} else {
|
||||
setFilters({ target_entity: null })
|
||||
}
|
||||
setOpen(false)
|
||||
},
|
||||
filters: filters.target_entity,
|
||||
typeKey: 'retention-table',
|
||||
singleMode: true,
|
||||
})
|
||||
|
||||
const entityLogicReturning = entityFilterLogic({
|
||||
setFilters: (filters: FilterType) => {
|
||||
if (filters.events && filters.events.length > 0) {
|
||||
setFilters({ returning_entity: filters.events[0] })
|
||||
} else if (filters.actions && filters.actions.length > 0) {
|
||||
setFilters({ returning_entity: filters.actions[0] })
|
||||
} else {
|
||||
setFilters({ returning_entity: null })
|
||||
}
|
||||
setReturningOpen(false)
|
||||
},
|
||||
filters: filters.returning_entity,
|
||||
typeKey: 'retention-table-returning',
|
||||
singleMode: true,
|
||||
})
|
||||
|
||||
const selectedRetainingEvent =
|
||||
filters.returning_entity?.name ||
|
||||
(filters.returning_entity.id && actionsLookup[filters.returning_entity.id]) ||
|
||||
'Select action'
|
||||
|
||||
const selectedCohortizingEvent =
|
||||
filters.target_entity?.name ||
|
||||
(filters.target_entity.id && actionsLookup[filters.target_entity.id]) ||
|
||||
'Select action'
|
||||
|
||||
// TODO: Update constant in retentionTableLogic.ts when releasing 4050
|
||||
const retentionOptions = {
|
||||
[`${RETENTION_FIRST_TIME}`]: 'for the first time',
|
||||
[`${RETENTION_RECURRING}`]: 'recurringly',
|
||||
}
|
||||
|
||||
return (
|
||||
<div data-attr="retention-tab" className="retention-tab">
|
||||
<Row gutter={16}>
|
||||
<Col md={16} xs={24}>
|
||||
<Row gutter={8} align="middle" className="mt">
|
||||
<Col>
|
||||
Showing <b>Unique users</b> who did
|
||||
</Col>
|
||||
<Col>
|
||||
<Button ref={node} data-attr="retention-action" onClick={() => setOpen(!open)}>
|
||||
<PropertyKeyInfo value={selectedCohortizingEvent} disablePopover />
|
||||
<DownOutlined
|
||||
className="svg-fix"
|
||||
style={{ marginRight: '-6px', marginTop: 2, color: '#bdbdbd', fontSize: '1.3em' }}
|
||||
/>
|
||||
</Button>
|
||||
<ActionFilterDropdown
|
||||
open={open}
|
||||
logic={entityLogic as any}
|
||||
openButtonRef={node}
|
||||
onClose={() => setOpen(false)}
|
||||
/>
|
||||
</Col>
|
||||
<Col>
|
||||
<div style={{ display: '-webkit-inline-box', flexWrap: 'wrap' }}>
|
||||
<Select
|
||||
value={retentionOptions[filters.retention_type]}
|
||||
onChange={(value): void => setFilters({ retention_type: value as RetentionType })}
|
||||
dropdownMatchSelectWidth={false}
|
||||
>
|
||||
{Object.entries(retentionOptions).map(([key, value]) => (
|
||||
<Select.Option key={key} value={key}>
|
||||
{value}
|
||||
<Tooltip placement="right" title={retentionOptionDescriptions[key]}>
|
||||
<InfoCircleOutlined className="info-indicator" />
|
||||
</Tooltip>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</div>
|
||||
</Col>
|
||||
<Col>grouped by</Col>
|
||||
<Col>
|
||||
<Select
|
||||
value={filters.period}
|
||||
onChange={(value): void => setFilters({ period: value })}
|
||||
dropdownMatchSelectWidth={false}
|
||||
>
|
||||
{dateOptions.map((period) => (
|
||||
<Select.Option key={period} value={period}>
|
||||
{period}
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={8} align="middle" className="mt">
|
||||
<Col>... who then came back and did</Col>
|
||||
<Col>
|
||||
<Button
|
||||
ref={returningNode}
|
||||
data-attr="retention-returning-action"
|
||||
onClick={(): void => setReturningOpen(!returningOpen)}
|
||||
>
|
||||
<PropertyKeyInfo value={selectedRetainingEvent} disablePopover />
|
||||
<DownOutlined
|
||||
className="svg-fix"
|
||||
style={{ marginRight: '-6px', marginTop: 2, color: '#bdbdbd', fontSize: '1.3em' }}
|
||||
/>
|
||||
</Button>
|
||||
<ActionFilterDropdown
|
||||
open={returningOpen}
|
||||
logic={entityLogicReturning as any}
|
||||
openButtonRef={returningNode}
|
||||
onClose={() => setReturningOpen(false)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
<p className="text-muted mt">
|
||||
Want to learn more about retention?{' '}
|
||||
<a
|
||||
href="https://posthog.com/docs/features/retention?utm_campaign=learn-more-horizontal&utm_medium=in-product"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
style={{ display: 'inline-flex', alignItems: 'center' }}
|
||||
>
|
||||
Go to docs
|
||||
<IconExternalLink style={{ marginLeft: 4 }} />
|
||||
</a>
|
||||
</p>
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col md={8} xs={24} style={{ marginTop: isSmallScreen ? '2rem' : 0 }}>
|
||||
<h4 className="secondary">Global Filters</h4>
|
||||
{filtersLoading ? (
|
||||
<Skeleton active paragraph={{ rows: 1 }} />
|
||||
) : (
|
||||
<>
|
||||
<PropertyFilters pageKey="insight-retention" />
|
||||
<TestAccountFilter filters={filters} onChange={setFilters} />
|
||||
</>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)
|
||||
}
|
@ -9,8 +9,15 @@ import { FilterType } from '~/types'
|
||||
import { Tooltip } from 'antd'
|
||||
import { InfoCircleOutlined } from '@ant-design/icons'
|
||||
import { TestAccountFilter } from '../TestAccountFilter'
|
||||
import { featureFlagLogic } from 'lib/logic/featureFlagLogic'
|
||||
import { SessionTabHorizontal } from './SessionTabHorizontal'
|
||||
|
||||
export function SessionTab(): JSX.Element {
|
||||
const { featureFlags } = useValues(featureFlagLogic)
|
||||
return featureFlags['4050-query-ui-optB'] ? <SessionTabHorizontal /> : <DefaultSessionTab />
|
||||
}
|
||||
|
||||
function DefaultSessionTab(): JSX.Element {
|
||||
const { filters } = useValues(trendsLogic({ dashboardItemId: null, view: ViewType.SESSIONS }))
|
||||
const { setFilters } = useActions(trendsLogic({ dashboardItemId: null, view: ViewType.SESSIONS }))
|
||||
|
||||
|
@ -0,0 +1,54 @@
|
||||
import React from 'react'
|
||||
import { useValues, useActions } from 'kea'
|
||||
import { PropertyFilters } from 'lib/components/PropertyFilters/PropertyFilters'
|
||||
import { SessionFilter } from 'lib/components/SessionsFilter'
|
||||
import { ViewType } from '../insightLogic'
|
||||
import { trendsLogic } from '../../trends/trendsLogic'
|
||||
import { ActionFilter } from '../ActionFilter/ActionFilter'
|
||||
import { FilterType } from '~/types'
|
||||
import { Col, Row, Skeleton } from 'antd'
|
||||
import { TestAccountFilter } from '../TestAccountFilter'
|
||||
import useBreakpoint from 'antd/lib/grid/hooks/useBreakpoint'
|
||||
|
||||
export function SessionTabHorizontal(): JSX.Element {
|
||||
const { filters, filtersLoading } = useValues(trendsLogic({ dashboardItemId: null, view: ViewType.SESSIONS }))
|
||||
const { setFilters } = useActions(trendsLogic({ dashboardItemId: null, view: ViewType.SESSIONS }))
|
||||
|
||||
const screens = useBreakpoint()
|
||||
const isSmallScreen = screens.xs || (screens.sm && !screens.md)
|
||||
|
||||
return (
|
||||
<Row gutter={16}>
|
||||
<Col md={16} xs={24}>
|
||||
<Row gutter={8} align="middle" className="mt mb">
|
||||
<Col>Showing</Col>
|
||||
<Col>
|
||||
<SessionFilter value={filters.session} onChange={(v: string) => setFilters({ session: v })} />
|
||||
</Col>
|
||||
<Col>where a user did any of the following:</Col>
|
||||
<Col />
|
||||
</Row>
|
||||
<ActionFilter
|
||||
filters={filters}
|
||||
setFilters={(payload: Partial<FilterType>) => setFilters(payload)}
|
||||
typeKey={'sessions' + ViewType.SESSIONS}
|
||||
hideMathSelector={true}
|
||||
buttonCopy="Add action or event"
|
||||
showOr={true}
|
||||
horizontalUI
|
||||
/>
|
||||
</Col>
|
||||
<Col md={8} xs={24} style={{ marginTop: isSmallScreen ? '2rem' : 0 }}>
|
||||
<h4 className="secondary">Global Filters</h4>
|
||||
{filtersLoading ? (
|
||||
<Skeleton active paragraph={{ rows: 1 }} />
|
||||
) : (
|
||||
<>
|
||||
<PropertyFilters pageKey="insight-retention" />
|
||||
<TestAccountFilter filters={filters} onChange={setFilters} />
|
||||
</>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
)
|
||||
}
|
@ -78,6 +78,14 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.retention-date-picker {
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
input::placeholder {
|
||||
color: $text_default;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.insight-empty-state {
|
||||
|
36
frontend/src/scenes/insights/RetentionDatePicker.tsx
Normal file
36
frontend/src/scenes/insights/RetentionDatePicker.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import React from 'react'
|
||||
import dayjs from 'dayjs'
|
||||
import dayjsGenerateConfig from 'rc-picker/lib/generate/dayjs'
|
||||
import generatePicker from 'antd/es/date-picker/generatePicker'
|
||||
import { useActions, useValues } from 'kea'
|
||||
import { retentionTableLogic } from 'scenes/retention/retentionTableLogic'
|
||||
import { CalendarOutlined } from '@ant-design/icons'
|
||||
import { Tooltip } from 'antd'
|
||||
|
||||
const DatePicker = generatePicker<dayjs.Dayjs>(dayjsGenerateConfig)
|
||||
|
||||
export function RetentionDatePicker(): JSX.Element {
|
||||
const { filters } = useValues(retentionTableLogic({ dashboardItemId: null }))
|
||||
const { setFilters } = useActions(retentionTableLogic({ dashboardItemId: null }))
|
||||
const yearSuffix = filters.date_to && dayjs(filters.date_to).year() !== dayjs().year() ? ', YYYY' : ''
|
||||
return (
|
||||
<>
|
||||
<Tooltip title="Cohorts up to this end date">
|
||||
<span style={{ maxWidth: 100, display: 'inline-flex', alignItems: 'center' }}>
|
||||
<CalendarOutlined />
|
||||
<DatePicker
|
||||
showTime={filters.period === 'Hour'}
|
||||
use12Hours
|
||||
format={filters.period === 'Hour' ? `MMM D${yearSuffix}, h a` : `MMM D${yearSuffix}`}
|
||||
value={filters.date_to ? dayjs(filters.date_to) : undefined}
|
||||
onChange={(date_to) => setFilters({ date_to: date_to && dayjs(date_to).toISOString() })}
|
||||
allowClear
|
||||
placeholder="Today"
|
||||
className="retention-date-picker"
|
||||
suffixIcon={null}
|
||||
/>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</>
|
||||
)
|
||||
}
|
@ -7,6 +7,7 @@ import { insightHistoryLogic } from 'scenes/insights/InsightHistoryPanel/insight
|
||||
import { pathsLogicType } from './pathsLogicType'
|
||||
import { FilterType, PropertyFilter } from '~/types'
|
||||
import { dashboardItemsModel } from '~/models/dashboardItemsModel'
|
||||
import { propertyDefinitionsLogic } from 'scenes/events/propertyDefinitionsLogic'
|
||||
|
||||
export const PAGEVIEW = '$pageview'
|
||||
export const SCREEN = '$screen'
|
||||
@ -14,10 +15,10 @@ export const AUTOCAPTURE = '$autocapture'
|
||||
export const CUSTOM_EVENT = 'custom_event'
|
||||
|
||||
export const pathOptionsToLabels = {
|
||||
[`${PAGEVIEW}`]: 'Pageview (Web)',
|
||||
[`${SCREEN}`]: 'Screen (Mobile)',
|
||||
[`${AUTOCAPTURE}`]: 'Autocaptured Events',
|
||||
[`${CUSTOM_EVENT}`]: 'Custom Events',
|
||||
[`${PAGEVIEW}`]: 'Page views (Web)',
|
||||
[`${SCREEN}`]: 'Screen views (Mobile)',
|
||||
[`${AUTOCAPTURE}`]: 'Autocaptured events',
|
||||
[`${CUSTOM_EVENT}`]: 'Custom events',
|
||||
}
|
||||
|
||||
export const pathOptionsToProperty = {
|
||||
@ -177,6 +178,10 @@ export const pathsLogic = kea<pathsLogicType<PathResult, PropertyFilter, FilterT
|
||||
return Object.keys(result).length === 0 ? '' : result
|
||||
},
|
||||
],
|
||||
filtersLoading: [
|
||||
() => [propertyDefinitionsLogic.selectors.loaded],
|
||||
(propertiesLoaded): boolean => !propertiesLoaded,
|
||||
],
|
||||
},
|
||||
actionToUrl: ({ values }) => ({
|
||||
setProperties: () => {
|
||||
|
@ -5,9 +5,9 @@ import { toParams, objectsEqual } from 'lib/utils'
|
||||
import { ViewType, insightLogic } from 'scenes/insights/insightLogic'
|
||||
import { insightHistoryLogic } from 'scenes/insights/InsightHistoryPanel/insightHistoryLogic'
|
||||
import { retentionTableLogicType } from './retentionTableLogicType'
|
||||
import { ACTIONS_LINE_GRAPH_LINEAR, ACTIONS_TABLE } from 'lib/constants'
|
||||
import { ACTIONS_LINE_GRAPH_LINEAR, ACTIONS_TABLE, RETENTION_FIRST_TIME, RETENTION_RECURRING } from 'lib/constants'
|
||||
import { actionsModel } from '~/models'
|
||||
import { ActionType } from '~/types'
|
||||
import { ActionType, FilterType } from '~/types'
|
||||
import {
|
||||
RetentionTablePayload,
|
||||
RetentionTrendPayload,
|
||||
@ -15,12 +15,11 @@ import {
|
||||
RetentionTrendPeoplePayload,
|
||||
} from 'scenes/retention/types'
|
||||
import { dashboardItemsModel } from '~/models/dashboardItemsModel'
|
||||
import { eventDefinitionsLogic } from 'scenes/events/eventDefinitionsLogic'
|
||||
import { propertyDefinitionsLogic } from 'scenes/events/propertyDefinitionsLogic'
|
||||
|
||||
export const dateOptions = ['Hour', 'Day', 'Week', 'Month']
|
||||
|
||||
const RETENTION_RECURRING = 'retention_recurring'
|
||||
const RETENTION_FIRST_TIME = 'retention_first_time'
|
||||
|
||||
export const retentionOptions = {
|
||||
[`${RETENTION_FIRST_TIME}`]: 'First Time',
|
||||
[`${RETENTION_RECURRING}`]: 'Recurring',
|
||||
@ -103,6 +102,7 @@ export const retentionTableLogic = kea<
|
||||
values: [actionsModel, ['actions']],
|
||||
},
|
||||
actions: () => ({
|
||||
// TODO: This needs to be properly typed with `FilterType`. N.B. We're currently mixing snake_case and pascalCase attribute names.
|
||||
setFilters: (filters: Record<string, any>) => ({ filters }),
|
||||
loadMorePeople: true,
|
||||
updatePeople: (people) => ({ people }),
|
||||
@ -114,7 +114,7 @@ export const retentionTableLogic = kea<
|
||||
filters: [
|
||||
props.filters
|
||||
? defaultFilters(props.filters as Record<string, any>)
|
||||
: (state: Record<string, any>) => defaultFilters(router.selectors.searchParams(state)),
|
||||
: (state) => defaultFilters(router.selectors.searchParams(state)),
|
||||
{
|
||||
setFilters: (state, { filters }) => ({ ...state, ...filters }),
|
||||
},
|
||||
@ -140,6 +140,10 @@ export const retentionTableLogic = kea<
|
||||
(selectors) => [(selectors as any).actions],
|
||||
(actions: ActionType[]) => Object.assign({}, ...actions.map((action) => ({ [action.id]: action.name }))),
|
||||
],
|
||||
filtersLoading: [
|
||||
() => [eventDefinitionsLogic.selectors.loaded, propertyDefinitionsLogic.selectors.loaded],
|
||||
(eventsLoaded, propertiesLoaded) => !eventsLoaded || !propertiesLoaded,
|
||||
],
|
||||
},
|
||||
events: ({ actions, props }) => ({
|
||||
afterMount: () => props.dashboardItemId && actions.loadResults(),
|
||||
@ -202,7 +206,7 @@ export const retentionTableLogic = kea<
|
||||
actions.updatePeople(newPeople)
|
||||
}
|
||||
},
|
||||
[dashboardItemsModel.actionTypes.refreshAllDashboardItems]: (filters: Record<string, any>) => {
|
||||
[dashboardItemsModel.actionTypes.refreshAllDashboardItems]: (filters: FilterType) => {
|
||||
if (props.dashboardItemId) {
|
||||
actions.setFilters(filters)
|
||||
}
|
||||
|
@ -304,7 +304,7 @@ export const trendsLogic = kea<
|
||||
selectors: () => ({
|
||||
filtersLoading: [
|
||||
() => [eventDefinitionsLogic.selectors.loaded, propertyDefinitionsLogic.selectors.loaded],
|
||||
(eventsLoaded, propertiesLoaded) => !eventsLoaded || !propertiesLoaded,
|
||||
(eventsLoaded, propertiesLoaded): boolean => !eventsLoaded || !propertiesLoaded,
|
||||
],
|
||||
results: [(selectors) => [selectors._results], (response) => response.result],
|
||||
resultsLoading: [(selectors) => [selectors._resultsLoading], (_resultsLoading) => _resultsLoading],
|
||||
|
@ -8,6 +8,8 @@ import {
|
||||
PAGEVIEW,
|
||||
SCREEN,
|
||||
ShownAsValue,
|
||||
RETENTION_RECURRING,
|
||||
RETENTION_FIRST_TIME,
|
||||
} from 'lib/constants'
|
||||
import { PluginConfigSchema } from '@posthog/plugin-scaffold'
|
||||
import { PluginInstallationType } from 'scenes/plugins/types'
|
||||
@ -507,7 +509,8 @@ export type InsightType = 'TRENDS' | 'SESSIONS' | 'FUNNELS' | 'RETENTION' | 'PAT
|
||||
export type ShownAsType = ShownAsValue // DEPRECATED: Remove when releasing `remove-shownas`
|
||||
export type BreakdownType = 'cohort' | 'person' | 'event'
|
||||
export type PathType = typeof PAGEVIEW | typeof AUTOCAPTURE | typeof SCREEN | typeof CUSTOM_EVENT
|
||||
export type RetentionType = 'retention_recurring' | 'retention_first_time'
|
||||
|
||||
export type RetentionType = typeof RETENTION_RECURRING | typeof RETENTION_FIRST_TIME
|
||||
|
||||
export interface FilterType {
|
||||
insight?: InsightType
|
||||
|
Loading…
Reference in New Issue
Block a user