0
0
mirror of https://github.com/PostHog/posthog.git synced 2024-11-24 09:14:46 +01:00

#4050 insights layouts & misc improvements (#4306)

This commit is contained in:
Paolo D'Amico 2021-05-12 03:38:01 -07:00 committed by GitHub
parent c7b71c4e53
commit ebddd25ba5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 488 additions and 51 deletions

View File

@ -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>

View File

@ -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"

View File

@ -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',

View File

@ -93,6 +93,7 @@ export function ActionFilter({
singleFilter={singleFilter}
showOr={showOr}
horizontalUI={horizontalUI}
filterCount={localFilters.length}
/>
))
)

View File

@ -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;

View File

@ -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">

View File

@ -43,6 +43,7 @@ export const SortableActionFilterRow = sortableElement(
key={filterIndex}
hideMathSelector={hideMathSelector}
hidePropertySelector={hidePropertySelector}
filterCount={filterCount}
/>
</div>
)

View File

@ -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

View File

@ -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 }))

View File

@ -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>
)
}

View File

@ -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)

View File

@ -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>
)
}

View File

@ -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 }))

View File

@ -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>
)
}

View File

@ -78,6 +78,14 @@
}
}
}
.retention-date-picker {
background-color: transparent;
border: 0;
input::placeholder {
color: $text_default;
}
}
}
.insight-empty-state {

View 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>
</>
)
}

View File

@ -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: () => {

View File

@ -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)
}

View File

@ -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],

View File

@ -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