Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Funnel improvements - Graph type, identical steps, UI updates #5384

Merged
merged 8 commits into from
Aug 2, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
@import '~/vars';

.cohort-type-selector {
.dropdown-selector {
padding: $default_spacing / 2;
border: 1px solid $border_light;
border-radius: $radius;
display: flex;
align-items: center;
cursor: pointer;

&.disabled {
color: $text_muted;
cursor: not-allowed;
}
}
89 changes: 89 additions & 0 deletions frontend/src/lib/components/DropdownSelector/DropdownSelector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/* Custom dropdown selector with an icon a help caption */
import { DownOutlined } from '@ant-design/icons'
import { Dropdown, Menu, Row } from 'antd'
import React from 'react'
import './DropdownSelector.scss'

interface DropdownSelectorProps {
label?: string
value: string | null
onValueChange: (value: string) => void
options: DropdownOption[]
hideDescriptionOnDisplay?: boolean // Hides the description support text on the main display component (i.e. only shown in the dropdown menu)
disabled?: boolean
}

interface DropdownOption {
key: string
label: string
description?: string
icon: JSX.Element
hidden?: boolean
}

interface SelectItemInterface {
icon: JSX.Element
label: string
description?: string
onClick: () => void
}

function SelectItem({ icon, label, description, onClick }: SelectItemInterface): JSX.Element {
return (
<div onClick={onClick}>
<Row align={'middle'}>
{icon}
<div style={{ fontSize: 14, fontWeight: 'bold', marginLeft: 4 }}>{label}</div>
</Row>
{description && <div style={{ fontSize: 12, color: 'rgba(0, 0, 0, 0.5)' }}>{description}</div>}
</div>
)
}

export function DropdownSelector({
label,
value,
onValueChange,
options,
hideDescriptionOnDisplay,
disabled,
}: DropdownSelectorProps): JSX.Element {
const selectedOption = options.find((opt) => opt.key === value)

const menu = (
<Menu>
{options.map(({ key, hidden, ...props }) => {
if (hidden) {
return null
}
return (
<Menu.Item key={key}>
<SelectItem {...props} onClick={() => onValueChange(key)} />
</Menu.Item>
)
})}
</Menu>
)

return (
<>
{label && <label className="ant-form-item-label">{label}</label>}
<Dropdown overlay={menu} trigger={['click']} disabled={disabled}>
<div className={`dropdown-selector${disabled ? ' disabled' : ''}`} onClick={(e) => e.preventDefault()}>
<div style={{ flexGrow: 1 }}>
{selectedOption && (
<SelectItem
{...selectedOption}
onClick={() => {}}
description={hideDescriptionOnDisplay ? undefined : selectedOption.description}
/>
)}
</div>
<div style={{ display: 'flex', alignItems: 'center' }}>
<DownOutlined />
</div>
</div>
</Dropdown>
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { isValidPropertyFilter } from 'lib/components/PropertyFilters/utils'
import { FEATURE_FLAGS } from 'lib/constants'
import { Popup } from 'lib/components/Popup/Popup'
import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types'
import { PlusCircleOutlined } from '@ant-design/icons'

interface FilterRowProps {
item: AnyPropertyFilter
Expand Down Expand Up @@ -118,9 +119,11 @@ export const FilterRow = React.memo(function FilterRow({
<Button
ref={setRef}
onClick={() => setOpen(!open)}
type="default"
shape="round"
className="new-prop-filter"
data-attr={'new-prop-filter-' + pageKey}
type="link"
style={{ paddingLeft: 0 }}
icon={<PlusCircleOutlined />}
>
Add filter
</Button>
Expand Down Expand Up @@ -164,7 +167,12 @@ export const FilterRow = React.memo(function FilterRow({
{isValidPropertyFilter(item) ? (
<PropertyFilterButton onClick={() => setOpen(!open)} item={item} />
) : (
<Button type="default" shape="round" data-attr={'new-prop-filter-' + pageKey}>
<Button
type="link"
data-attr={'new-prop-filter-' + pageKey}
style={{ paddingLeft: 0 }}
icon={<PlusCircleOutlined />}
>
Add filter
</Button>
)}
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/lib/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -264,3 +264,7 @@ export const ERROR_MESSAGES: Record<string, string> = {
no_new_organizations:
'Your email address is not associated with an account. Please ask your administrator for an invite.',
}

// Cohort types
export const COHORT_STATIC = 'static'
export const COHORT_DYNAMIC = 'dynamic'
78 changes: 0 additions & 78 deletions frontend/src/scenes/cohorts/CohortV2/CohortTypeSelector.tsx

This file was deleted.

31 changes: 26 additions & 5 deletions frontend/src/scenes/cohorts/CohortV2/CohortV2.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,20 @@ import React from 'react'
import { useActions, useValues } from 'kea'
import { CohortNameInput } from './CohortNameInput'
import { CohortDescriptionInput } from './CohortDescriptionInput'
import { CohortTypeSelector, DYNAMIC, STATIC } from './CohortTypeSelector'
import { Button, Col, Divider, Row, Spin } from 'antd'
import { CohortMatchingCriteriaSection } from './CohortMatchingCriteriaSection'
import { CohortGroupType, CohortType } from '~/types'
import { ENTITY_MATCH_TYPE, PROPERTY_MATCH_TYPE } from 'lib/constants'
import { COHORT_DYNAMIC, COHORT_STATIC, ENTITY_MATCH_TYPE, PROPERTY_MATCH_TYPE } from 'lib/constants'
import { InboxOutlined, DeleteOutlined, SaveOutlined, LoadingOutlined } from '@ant-design/icons'
import Dragger from 'antd/lib/upload/Dragger'
import { CohortDetailsRow } from './CohortDetailsRow'
import { Persons } from 'scenes/persons/Persons'
import { cohortLogic } from './cohortLogic'
import { UploadFile } from 'antd/lib/upload/interface'

import { CalculatorOutlined, OrderedListOutlined } from '@ant-design/icons'
import { DropdownSelector } from 'lib/components/DropdownSelector/DropdownSelector'

export function CohortV2(props: { cohort: CohortType }): JSX.Element {
const logic = cohortLogic(props)
const { setCohort } = useActions(logic)
Expand Down Expand Up @@ -68,12 +70,12 @@ export function CohortV2(props: { cohort: CohortType }): JSX.Element {
}

const onTypeChange = (type: string): void => {
if (type === STATIC) {
if (type === COHORT_STATIC) {
setCohort({
...cohort,
is_static: true,
})
} else if (type === DYNAMIC) {
} else if (type === COHORT_DYNAMIC) {
setCohort({
...cohort,
is_static: false,
Expand All @@ -93,6 +95,21 @@ export function CohortV2(props: { cohort: CohortType }): JSX.Element {
accept: '.csv',
}

const COHORT_TYPE_OPTIONS = [
{
key: COHORT_STATIC,
label: 'Static',
description: 'Upload a list of users. Updates manually',
icon: <OrderedListOutlined />,
},
{
key: COHORT_DYNAMIC,
label: 'Dynamic',
description: 'Cohort updates dynamically based on properties',
icon: <CalculatorOutlined />,
},
]

return (
<div className="mb">
<Row gutter={16}>
Expand All @@ -105,7 +122,11 @@ export function CohortV2(props: { cohort: CohortType }): JSX.Element {
<CohortNameInput input={cohort.name} onChange={onNameChange} />
</Col>
<Col md={10}>
<CohortTypeSelector type={cohort.is_static ? STATIC : DYNAMIC} onTypeChange={onTypeChange} />
<DropdownSelector
options={COHORT_TYPE_OPTIONS}
value={cohort.is_static ? COHORT_STATIC : COHORT_DYNAMIC}
onValueChange={onTypeChange}
/>
</Col>
</Row>
<Row gutter={16} className="mt">
Expand Down
34 changes: 30 additions & 4 deletions frontend/src/scenes/funnels/FunnelBarGraph.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, { ForwardRefRenderFunction, useEffect, useRef, useState } from 'react'
import { humanFriendlyDuration, humanizeNumber } from 'lib/utils'
import { PropertyKeyInfo } from 'lib/components/PropertyKeyInfo'
import { Button, ButtonProps, Popover } from 'antd'
import { ArrowRightOutlined } from '@ant-design/icons'
import { Button, ButtonProps, Popover, Tooltip } from 'antd'
import { ArrowRightOutlined, InfoCircleOutlined } from '@ant-design/icons'
import { useResizeObserver } from 'lib/utils/responsiveUtils'
import { SeriesGlyph } from 'lib/components/SeriesGlyph'
import { ArrowBottomRightOutlined } from 'lib/components/icons'
Expand Down Expand Up @@ -40,6 +40,27 @@ interface BarProps {

type LabelPosition = 'inside' | 'outside'

function DuplicateStepIndicator(): JSX.Element {
return (
<span style={{ marginLeft: 4 }}>
<Tooltip
title={
<>
<b>Sequential &amp; Repeated Events</b>
<p>
When an event is repeated across funnel steps, it is interpreted as a sequence. For example,
a three-step funnel consisting of pageview events is interpretted as first pageview,
followed by second pageview, followed by a third pageview.
</p>
</>
}
>
<InfoCircleOutlined />
</Tooltip>
</span>
)
}

function Bar({
percentage,
name,
Expand Down Expand Up @@ -310,8 +331,13 @@ export function FunnelBarGraph({ filters, dashboardItemId, color = 'white' }: Om
<div className={`funnel-series-linebox ${showLineAfter ? 'after' : ''}`} />
</div>
<header>
<div className="funnel-step-title">
<PropertyKeyInfo value={step.name} style={{ maxWidth: '100%' }} />
<div style={{ display: 'flex', maxWidth: '100%', flexGrow: 1 }}>
<div className="funnel-step-title">
<PropertyKeyInfo value={step.name} style={{ maxWidth: '100%' }} />
</div>
{clickhouseFeaturesEnabled && i > 0 && step.action_id === steps[i - 1].action_id && (
<DuplicateStepIndicator />
)}
</div>
<div className={`funnel-step-metadata funnel-time-metadata ${layout}`}>
{step.average_conversion_time && step.average_conversion_time >= 0 + Number.EPSILON ? (
Expand Down
26 changes: 26 additions & 0 deletions frontend/src/scenes/funnels/FunnelViz.scss
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,29 @@
display: none;
}
}

.funnel-tab {
.property-filter-wrapper {
//margin-left: 49px; // 30px draggable icon width + 4px margin-left + 15px button padding-left
display: flex;
flex-direction: row;

.arrow {
color: #c4c4c4;
font-size: 18px;
min-width: 30px;
height: 48px;
box-sizing: border-box;
align-items: center;
padding-left: 4px;
padding-right: 4px;
position: relative;
top: 6px;
user-select: none;
}

.new-prop-filter {
padding-left: 16px !important;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,8 @@ export function ActionFilterRow({
)}

{visible && (
<div className="ml mr">
<div className="mr property-filter-wrapper">
<div className="arrow">&#8627;</div>
<PropertyFilters
pageKey={`${index}-${value}-filter`}
propertyFilters={filter.properties}
Expand Down
Loading