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

#477 filter per entity #671

Merged
merged 21 commits into from
May 4, 2020
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
15 changes: 3 additions & 12 deletions frontend/src/lib/components/PropertyFilters/PropertyFilter.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,10 @@
import React, { Component } from 'react'
import React from 'react'
import Select from 'react-select'
import { selectStyle } from '../../utils'
import { selectStyle, operatorMap } from '../../utils'
import { PropertyValue } from './PropertyValue'
import { useValues, useActions } from 'kea'
import { propertyFilterLogic } from './propertyFilterLogic'

export const operatorMap = {
null: 'equals',
is_not: "doesn't equal",
icontains: 'contains',
not_icontains: "doesn't contain",
gt: 'greater than',
lt: 'lower than',
}
const operatorOptions = Object.entries(operatorMap).map(([key, value]) => ({
label: value,
value: key,
Expand All @@ -24,7 +16,6 @@ export function PropertyFilter({ index, endpoint, onChange, pageKey, onComplete
let item = filters[index]
let key = Object.keys(item)[0] ? Object.keys(item)[0].split('__') : []
let value = Object.values(item)[0]

return (
<div className="row" style={{ margin: '0.5rem -15px', minWidth: key[0] ? 700 : 200 }}>
{properties && (
Expand Down Expand Up @@ -55,7 +46,7 @@ export function PropertyFilter({ index, endpoint, onChange, pageKey, onComplete
style={{ width: 200 }}
value={[
{
label: operatorMap[key[1]] || 'equals',
label: operatorMap[key[1]] || '= equals',
value: key[1],
},
]}
Expand Down
15 changes: 3 additions & 12 deletions frontend/src/lib/components/PropertyFilters/PropertyFilters.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,10 @@
import React, { useState } from 'react'
import { PropertyFilter, operatorMap } from './PropertyFilter'
import { PropertyFilter } from './PropertyFilter'
import { Button } from 'antd'
import { useValues, useActions } from 'kea'
import { propertyFilterLogic } from './propertyFilterLogic'
import { Popover, Row } from 'antd'
import { CloseButton } from '../../utils'

const operatorEntries = Object.entries(operatorMap).reverse()

const formatFilterName = str => {
for (let [key, value] of operatorEntries) {
if (str.includes(key)) return str.replace('__' + key, '') + ` ${value} `
}
return str + ` ${operatorMap['null']} `
}
import { CloseButton, formatFilterName } from '../../utils'

function FilterRow({ endpoint, propertyFilters, item, index, onChange, pageKey, filters }) {
const { remove } = useActions(propertyFilterLogic({ propertyFilters, endpoint, onChange, pageKey }))
Expand Down Expand Up @@ -53,7 +44,7 @@ function FilterRow({ endpoint, propertyFilters, item, index, onChange, pageKey,
</Button>
) : (
<Button type="default" shape="round">
{'Add Filter'}
{'New Filter'}
</Button>
)}
</Popover>
Expand Down
18 changes: 18 additions & 0 deletions frontend/src/lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,21 @@ export let debounce = (func, wait, immediate) => {
export const capitalizeFirstLetter = string => {
return string.charAt(0).toUpperCase() + string.slice(1)
}

export const operatorMap = {
exact: '= equals',
is_not: "≠ doesn't equal",
icontains: '∋ contains',
not_icontains: "∌ doesn't contain",
gt: '> greater than',
lt: '< lower than',
}

const operatorEntries = Object.entries(operatorMap).reverse()

export const formatFilterName = str => {
for (let [key, value] of operatorEntries) {
if (str.includes(key)) return str.replace('__' + key, '') + ` ${value.split(' ')[0]} `
}
return str + ` ${operatorMap['exact'].split(' ')[0]} `
}
12 changes: 2 additions & 10 deletions frontend/src/scenes/trends/ActionFilter/ActionFilter.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,15 @@ import { entityFilterLogic } from './actionFilterLogic'
import { ActionFilterRow } from './ActionFilterRow'
import { Button } from 'antd'

export function ActionFilter({ setFilters, defaultFilters, showMaths, typeKey, setDefaultIfEmpty }) {
export function ActionFilter({ setFilters, defaultFilters, typeKey, setDefaultIfEmpty }) {
const { allFilters } = useValues(entityFilterLogic({ setFilters, defaultFilters, typeKey, setDefaultIfEmpty }))
const { createNewFilter } = useActions(entityFilterLogic({ typeKey }))

return (
<div>
{allFilters &&
allFilters.map((filter, index) => {
return (
<ActionFilterRow
filter={filter}
index={index}
key={index}
showMaths={showMaths}
typeKey={typeKey}
/>
)
return <ActionFilterRow filter={filter} index={index} key={index} typeKey={typeKey} />
})}
<Button type="primary" onClick={() => createNewFilter()} style={{ marginTop: '0.5rem' }}>
Add action/event
Expand Down
41 changes: 35 additions & 6 deletions frontend/src/scenes/trends/ActionFilter/ActionFilterRow.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,33 @@
import React, { useRef } from 'react'
import React, { useRef, useState } from 'react'
import { useActions, useValues } from 'kea'
import { entityFilterLogic } from './actionFilterLogic'
import { EntityTypes } from '../trendsLogic'
import { CloseButton } from '~/lib/utils'
import { Dropdown } from '~/lib/components/Dropdown'
import { ActionFilterDropdown } from './ActionFilterDropdown'
import { Tooltip } from 'antd'
import { PropertyFilters } from 'lib/components/PropertyFilters/PropertyFilters'
import { userLogic } from 'scenes/userLogic'

export function ActionFilterRow({ filter, index, showMaths, typeKey }) {
const determineFilterLabel = (visible, filter) => {
if (visible) return 'Hide Filters'

if (filter.properties && Object.keys(filter.properties).length > 0) {
return (
Object.keys(filter.properties).length + ' Filter' + (Object.keys(filter.properties).length == 1 ? '' : 's')
)
}
return 'Add Filters'
}

export function ActionFilterRow({ filter, index, typeKey }) {
const node = useRef()
const { selectedFilter, entities } = useValues(entityFilterLogic({ typeKey }))
const { selectFilter, updateFilterMath, removeLocalFilter } = useActions(entityFilterLogic({ typeKey }))
const { selectFilter, updateFilterMath, removeLocalFilter, updateFilterProperty } = useActions(
entityFilterLogic({ typeKey })
)
const { eventProperties } = useValues(userLogic)
const [entityFilterVisible, setEntityFilterVisible] = useState(false)

let entity, dropDownCondition, onClick, onClose, onMathSelect, name, value, math
math = filter.math
Expand All @@ -36,7 +53,6 @@ export function ActionFilterRow({ filter, index, showMaths, typeKey }) {
name = entity.name || filter.name
value = entity.id || filter.id
}

return (
<div>
<button
Expand All @@ -54,8 +70,10 @@ export function ActionFilterRow({ filter, index, showMaths, typeKey }) {
>
{name || 'Select action'}
</button>
{showMaths && <MathSelector math={math} index={index} onMathSelect={onMathSelect} />}

<MathSelector math={math} index={index} onMathSelect={onMathSelect} />
<div className="btn btn-sm btn-light" onClick={() => setEntityFilterVisible(!entityFilterVisible)}>
{determineFilterLabel(entityFilterVisible, filter)}
</div>
<CloseButton
onClick={onClose}
style={{
Expand All @@ -65,6 +83,17 @@ export function ActionFilterRow({ filter, index, showMaths, typeKey }) {
marginTop: 3,
}}
/>
{entityFilterVisible && (
<div className="ml-4">
<PropertyFilters
pageKey={`${index}-${value}-filter`}
properties={eventProperties}
propertyFilters={filter.properties}
onChange={properties => updateFilterProperty({ properties, index })}
style={{ marginBottom: 0 }}
/>
</div>
)}
{dropDownCondition() && (
<ActionFilterDropdown
typeKey={typeKey}
Expand Down
19 changes: 6 additions & 13 deletions frontend/src/scenes/trends/ActionFilter/actionFilterLogic.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,8 @@
import { kea } from 'kea'

import { actionsModel } from '~/models/actionsModel'
import { EntityTypes } from '../trendsLogic'

import { groupEvents } from '~/lib/utils'
import { userLogic } from 'scenes/userLogic'

const mirrorValues = (entities, newKey) => {
let newEntities = entities.map(entity => {
return {
...entity,
[newKey]: entity,
}
})
return newEntities
}

export const entityFilterLogic = kea({
key: props => props.typeKey,
connect: {
Expand All @@ -33,6 +20,7 @@ export const entityFilterLogic = kea({
removeLocalFilter: filter => ({ value: filter.value, type: filter.type, index: filter.index }),
createNewFilter: true,
setLocalFilters: filters => ({ filters }),
updateFilterProperty: filter => ({ properties: filter.properties, index: filter.index }),
}),

reducers: ({ actions, props }) => ({
Expand Down Expand Up @@ -86,6 +74,11 @@ export const entityFilterLogic = kea({
actions.setLocalFilters(currentfilters)
actions.selectFilter(null)
},
[actions.updateFilterProperty]: ({ properties, index }) => {
let currentfilters = values.allFilters ? [...values.allFilters] : []
currentfilters[index].properties = properties
actions.setLocalFilters(currentfilters)
},
[actions.updateFilterMath]: ({ math, index }) => {
let currentfilters = values.allFilters ? [...values.allFilters] : []
currentfilters[index].math = math
Expand Down
25 changes: 14 additions & 11 deletions frontend/src/scenes/trends/LineGraph.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import React, { Component } from 'react'
import Chart from 'chart.js'
import PropTypes from 'prop-types'
import { formatFilterName } from '~/lib/utils'
import _ from 'lodash'

//--Chart Style Options--//
// Chart.defaults.global.defaultFontFamily = "'PT Sans', sans-serif"
Expand Down Expand Up @@ -103,18 +105,19 @@ export class LineGraph extends Component {
titleSpacing: 0,
callbacks: {
label: function(tooltipItem, data) {
if (
data.datasets[tooltipItem.datasetIndex].dotted &&
!(
tooltipItem.index ==
data.datasets[tooltipItem.datasetIndex].data.length - 1
)
)
let entityData = data.datasets[tooltipItem.datasetIndex]
if (entityData.dotted && !(tooltipItem.index == entityData.data.length - 1))
return null
var label =
data.datasets[tooltipItem.datasetIndex].chartLabel ||
data.datasets[tooltipItem.datasetIndex].label ||
''
var label = entityData.chartLabel || entityData.label || ''
if (entityData.action.properties && !_.isEmpty(entityData.action.properties)) {
label += ' ('
Object.entries(entityData.action.properties).forEach(([key, val], index) => {
if (index > 0) label += ', '
label += formatFilterName(key).split(' ')[1] + ' ' + val
})
label += ')'
}

return label + ' - ' + tooltipItem.yLabel.toLocaleString()
},
},
Expand Down
1 change: 0 additions & 1 deletion frontend/src/scenes/trends/Trends.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ export function Trends() {
setDefaultIfEmpty={true}
setFilters={setFilters}
defaultFilters={filters}
showMaths={true}
typeKey="trends"
/>
<hr />
Expand Down
19 changes: 11 additions & 8 deletions posthog/api/action.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ def _group_events_to_date(self, date_from: datetime.datetime, date_to: datetime.
response['total'] = {key: value[0] if len(value) > 0 else 0 for key, value in dataframe.iterrows()}
return response

def _filter_events(self, filter: Filter) -> Q:
def _filter_events(self, filter: Filter, entity: Optional[Entity]=None) -> Q:
filters = Q()
if filter.date_from:
filters &= Q(timestamp__gte=filter.date_from)
Expand All @@ -183,6 +183,8 @@ def _filter_events(self, filter: Filter) -> Q:
filters &= Q(timestamp__lte=filter.date_to + relativity)
if filter.properties:
filters &= properties_to_Q(filter.properties)
if entity and entity.properties:
filters &= properties_to_Q(entity.properties)
return filters

def _append_data(self, dates_filled: pd.DataFrame, interval: str) -> Dict:
Expand Down Expand Up @@ -255,11 +257,11 @@ def _execute_custom_sql(self, query, params):
cursor.execute(query, params)
return cursor.fetchall()

def _stickiness(self, filtered_events: QuerySet, filter: Filter) -> Dict[str, Any]:
def _stickiness(self, filtered_events: QuerySet, entity: Entity, filter: Filter) -> Dict[str, Any]:
range_days = (filter.date_to - filter.date_from).days + 2 if filter.date_from and filter.date_to else 90

events = filtered_events\
.filter(self._filter_events(filter))\
.filter(self._filter_events(filter, entity))\
.values('person_id') \
.annotate(day_count=Count(functions.TruncDay('timestamp'), distinct=True))\
.filter(day_count__lte=range_days)
Expand Down Expand Up @@ -296,7 +298,8 @@ def _serialize_entity(self, entity: Entity, filter: Filter, request: request.Req
'action': {
'id': entity.id,
'name': entity.name,
'type': entity.type
'type': entity.type,
'properties': entity.properties
},
'label': entity.name,
'count': 0,
Expand All @@ -306,7 +309,7 @@ def _serialize_entity(self, entity: Entity, filter: Filter, request: request.Req
}
response = []
events = self._process_entity_for_events(entity=entity, team=team, order_by=None if request.GET.get('shown_as') == 'Stickiness' else '-timestamp')
events = events.filter(self._filter_events(filter))
events = events.filter(self._filter_events(filter, entity))

if request.GET.get('shown_as', 'Volume') == 'Volume':
items = self._aggregate_by_interval(
Expand All @@ -326,7 +329,7 @@ def _serialize_entity(self, entity: Entity, filter: Filter, request: request.Req
response.append(new_dict)
elif request.GET['shown_as'] == 'Stickiness':
new_dict = copy.deepcopy(serialized)
new_dict.update(self._stickiness(filtered_events=events, filter=filter))
new_dict.update(self._stickiness(filtered_events=events, entity=entity, filter=filter))
response.append(new_dict)

return response
Expand Down Expand Up @@ -409,15 +412,15 @@ def _calculate_people(events: QuerySet):
})
if entity.type == TREND_FILTER_TYPE_EVENTS:
filtered_events = self._process_entity_for_events(entity, team=team, order_by=None)\
.filter(self._filter_events(filter))
.filter(self._filter_events(filter, entity))
elif entity.type == TREND_FILTER_TYPE_ACTIONS:
actions = super().get_queryset()
actions = actions.filter(deleted=False)
try:
action = actions.get(pk=entity.id)
except Action.DoesNotExist:
return Response([])
filtered_events = self._process_entity_for_events(entity, team=team, order_by=None).filter(self._filter_events(filter))
filtered_events = self._process_entity_for_events(entity, team=team, order_by=None).filter(self._filter_events(filter, entity))

people = _calculate_people(events=filtered_events)
return Response([people])
Loading