feat: rule editing (#31579)
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 4.9 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 5.7 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 5.1 KiB After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 7.1 KiB |
@@ -227,7 +227,6 @@ export const definitionPopoverLogic = kea<definitionPopoverLogicType>([
|
||||
TaxonomicFilterGroupType.NumericalEventProperties,
|
||||
TaxonomicFilterGroupType.Metadata,
|
||||
TaxonomicFilterGroupType.DataWarehousePersonProperties,
|
||||
TaxonomicFilterGroupType.ErrorTrackingIssueProperties,
|
||||
].includes(type) || type.startsWith(TaxonomicFilterGroupType.GroupsPrefix),
|
||||
],
|
||||
hasSentAs: [
|
||||
|
||||
@@ -242,6 +242,7 @@ export function PropertiesTable({
|
||||
[PropertyDefinitionType.Session]: TaxonomicFilterGroupType.SessionProperties,
|
||||
[PropertyDefinitionType.LogEntry]: TaxonomicFilterGroupType.LogEntries,
|
||||
[PropertyDefinitionType.Meta]: TaxonomicFilterGroupType.Metadata,
|
||||
[PropertyDefinitionType.Resource]: TaxonomicFilterGroupType.Resources,
|
||||
}
|
||||
|
||||
const propertyType = propertyTypeMap[type] || TaxonomicFilterGroupType.EventProperties
|
||||
|
||||
@@ -4,7 +4,14 @@ import { BindLogic, useActions, useValues } from 'kea'
|
||||
import { objectsEqual } from 'lib/utils'
|
||||
import { CSSProperties, useEffect } from 'react'
|
||||
|
||||
import { AnyPropertyFilter, EmptyPropertyFilter, PropertyFilterType, PropertyOperator } from '~/types'
|
||||
import {
|
||||
AnyPropertyFilter,
|
||||
EmptyPropertyFilter,
|
||||
PropertyFilterBaseValue,
|
||||
PropertyFilterType,
|
||||
PropertyFilterValue,
|
||||
PropertyOperator,
|
||||
} from '~/types'
|
||||
|
||||
import { SimpleOption, TaxonomicFilterGroupType } from '../TaxonomicFilter/types'
|
||||
import { PathItemSelector } from './components/PathItemSelector'
|
||||
@@ -74,7 +81,7 @@ export function PathItemFilters({
|
||||
remove(index)
|
||||
}}
|
||||
>
|
||||
{filter.value.toString()}
|
||||
{humanizeFilterValue(filter.value)}
|
||||
</PropertyFilterButton>
|
||||
)}
|
||||
</PathItemSelector>
|
||||
@@ -84,3 +91,11 @@ export function PathItemFilters({
|
||||
</BindLogic>
|
||||
)
|
||||
}
|
||||
|
||||
const humanizeFilterValue = (value: PropertyFilterValue): string => {
|
||||
const humanizeValue = (v: PropertyFilterBaseValue): string => {
|
||||
return typeof v === 'object' ? v.id.toString() : v.toString()
|
||||
}
|
||||
|
||||
return value === null ? 'None' : Array.isArray(value) ? value.map(humanizeValue).join(', ') : humanizeValue(value)
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ interface PropertyFiltersProps {
|
||||
orFiltering?: boolean
|
||||
propertyGroupType?: FilterLogicalOperator | null
|
||||
addText?: string | null
|
||||
editable?: boolean
|
||||
buttonText?: string
|
||||
buttonSize?: LemonButtonProps['size']
|
||||
hasRowOperator?: boolean
|
||||
@@ -67,6 +68,7 @@ export function PropertyFilters({
|
||||
propertyGroupType = null,
|
||||
addText = null,
|
||||
buttonText = 'Add filter',
|
||||
editable = true,
|
||||
buttonSize,
|
||||
hasRowOperator = true,
|
||||
sendAllKeyUpdates = false,
|
||||
@@ -105,7 +107,7 @@ export function PropertyFilters({
|
||||
)}
|
||||
<div className="PropertyFilters__content max-w-full">
|
||||
<BindLogic logic={propertyFilterLogic} props={logicProps}>
|
||||
{(allowNew ? filtersWithNew : filters).map((item: AnyPropertyFilter, index: number) => {
|
||||
{(allowNew && editable ? filtersWithNew : filters).map((item: AnyPropertyFilter, index: number) => {
|
||||
return (
|
||||
<React.Fragment key={index}>
|
||||
{logicalRowDivider && index > 0 && index !== filtersWithNew.length - 1 && (
|
||||
@@ -123,6 +125,7 @@ export function PropertyFilters({
|
||||
label={buttonText}
|
||||
onRemove={remove}
|
||||
orFiltering={orFiltering}
|
||||
editable={editable}
|
||||
filterComponent={(onComplete) => (
|
||||
<TaxonomicPropertyFilter
|
||||
key={index}
|
||||
@@ -148,6 +151,7 @@ export function PropertyFilters({
|
||||
hideBehavioralCohorts={hideBehavioralCohorts}
|
||||
size={buttonSize}
|
||||
addFilterDocLink={addFilterDocLink}
|
||||
editable={editable}
|
||||
/>
|
||||
)}
|
||||
errorMessage={errorMessages && errorMessages[index]}
|
||||
|
||||
@@ -27,6 +27,7 @@ interface FilterRowProps {
|
||||
orFiltering?: boolean
|
||||
errorMessage?: JSX.Element | null
|
||||
disabledReason?: string
|
||||
editable: boolean
|
||||
}
|
||||
|
||||
export const FilterRow = React.memo(function FilterRow({
|
||||
@@ -44,6 +45,7 @@ export const FilterRow = React.memo(function FilterRow({
|
||||
orFiltering,
|
||||
errorMessage,
|
||||
disabledReason,
|
||||
editable,
|
||||
}: FilterRowProps) {
|
||||
const [open, setOpen] = useState(() => openOnInsert)
|
||||
|
||||
@@ -73,7 +75,7 @@ export const FilterRow = React.memo(function FilterRow({
|
||||
{disablePopover ? (
|
||||
<>
|
||||
{filterComponent(() => setOpen(false))}
|
||||
{!!Object.keys(filters[index]).length && (
|
||||
{Object.keys(filters[index]).length > 0 && editable ? (
|
||||
<LemonButton
|
||||
icon={orFiltering ? <IconTrash /> : <IconX />}
|
||||
onClick={() => onRemove(index)}
|
||||
@@ -81,7 +83,7 @@ export const FilterRow = React.memo(function FilterRow({
|
||||
className="ml-2"
|
||||
noPadding
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<Popover
|
||||
|
||||
@@ -19,19 +19,21 @@ const makePropertyDefinition = (name: string, propertyType: PropertyType | undef
|
||||
description: '',
|
||||
})
|
||||
|
||||
const props = (type?: PropertyType | undefined): OperatorValueSelectProps => ({
|
||||
const props = (type: PropertyType | undefined, editable: boolean): OperatorValueSelectProps => ({
|
||||
type: undefined,
|
||||
propertyKey: 'the_property',
|
||||
onChange: () => {},
|
||||
propertyDefinitions: [makePropertyDefinition('the_property', type)],
|
||||
defaultOpen: true,
|
||||
editable,
|
||||
})
|
||||
|
||||
export function OperatorValueWithStringProperty(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<h1>String Property</h1>
|
||||
<OperatorValueSelect {...props(PropertyType.String)} />
|
||||
<OperatorValueSelect {...props(PropertyType.String, true)} />
|
||||
<OperatorValueSelect {...props(PropertyType.String, false)} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -40,7 +42,8 @@ export function OperatorValueWithDateTimeProperty(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<h1>Date Time Property</h1>
|
||||
<OperatorValueSelect {...props(PropertyType.DateTime)} />
|
||||
<OperatorValueSelect {...props(PropertyType.DateTime, true)} />
|
||||
<OperatorValueSelect {...props(PropertyType.DateTime, false)} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -49,7 +52,8 @@ export function OperatorValueWithNumericProperty(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<h1>Numeric Property</h1>
|
||||
<OperatorValueSelect {...props(PropertyType.Numeric)} />
|
||||
<OperatorValueSelect {...props(PropertyType.Numeric, true)} />
|
||||
<OperatorValueSelect {...props(PropertyType.Numeric, false)} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -58,7 +62,8 @@ export function OperatorValueWithBooleanProperty(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<h1>Boolean Property</h1>
|
||||
<OperatorValueSelect {...props(PropertyType.Boolean)} />
|
||||
<OperatorValueSelect {...props(PropertyType.Boolean, true)} />
|
||||
<OperatorValueSelect {...props(PropertyType.Boolean, false)} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -67,7 +72,8 @@ export function OperatorValueWithSelectorProperty(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<h1>CSS Selector Property</h1>
|
||||
<OperatorValueSelect {...props(PropertyType.Selector)} />
|
||||
<OperatorValueSelect {...props(PropertyType.Selector, true)} />
|
||||
<OperatorValueSelect {...props(PropertyType.Selector, false)} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -76,7 +82,8 @@ export function OperatorValueWithUnknownProperty(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<h1>Property without specific type</h1>
|
||||
<OperatorValueSelect {...props()} />
|
||||
<OperatorValueSelect {...props(undefined, true)} />
|
||||
<OperatorValueSelect {...props(undefined, false)} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { LemonButtonProps, LemonSelect, LemonSelectProps } from '@posthog/lemon-ui'
|
||||
import { allOperatorsToHumanName } from 'lib/components/DefinitionPopover/utils'
|
||||
import { dayjs } from 'lib/dayjs'
|
||||
import {
|
||||
allOperatorsMapping,
|
||||
@@ -27,7 +28,8 @@ export interface OperatorValueSelectProps {
|
||||
type?: PropertyFilterType
|
||||
propertyKey?: string
|
||||
operator?: PropertyOperator | null
|
||||
value?: string | number | bigint | Array<string | number | bigint> | null
|
||||
value?: PropertyFilterValue
|
||||
editable: boolean
|
||||
placeholder?: string
|
||||
endpoint?: string
|
||||
onChange: (operator: PropertyOperator, value: PropertyFilterValue) => void
|
||||
@@ -81,6 +83,7 @@ export function OperatorValueSelect({
|
||||
addRelativeDateTimeOptions,
|
||||
groupTypeIndex = undefined,
|
||||
size,
|
||||
editable,
|
||||
}: OperatorValueSelectProps): JSX.Element {
|
||||
const propertyDefinition = propertyDefinitions.find((pd) => pd.name === propertyKey)
|
||||
|
||||
@@ -110,7 +113,10 @@ export function OperatorValueSelect({
|
||||
propertyType = PropertyType.Selector
|
||||
} else if (propertyKey === 'id' && type === PropertyFilterType.Cohort) {
|
||||
propertyType = PropertyType.Cohort
|
||||
} else if (propertyKey === 'assignee' && type === PropertyFilterType.ErrorTrackingIssue) {
|
||||
propertyType = PropertyType.Assignee
|
||||
}
|
||||
|
||||
const operatorMapping: Record<string, string> = chooseOperatorMap(propertyType)
|
||||
|
||||
const operators = Object.keys(operatorMapping) as Array<PropertyOperator>
|
||||
@@ -133,39 +139,43 @@ export function OperatorValueSelect({
|
||||
return (
|
||||
<>
|
||||
<div data-attr="taxonomic-operator">
|
||||
<OperatorSelect
|
||||
operator={currentOperator || PropertyOperator.Exact}
|
||||
operators={operators}
|
||||
onChange={(newOperator: PropertyOperator) => {
|
||||
const tentativeValidationError =
|
||||
newOperator && value ? getValidationError(newOperator, value, propertyKey) : null
|
||||
if (tentativeValidationError) {
|
||||
setValidationError(tentativeValidationError)
|
||||
return
|
||||
}
|
||||
setValidationError(null)
|
||||
{editable ? (
|
||||
<OperatorSelect
|
||||
operator={currentOperator || PropertyOperator.Exact}
|
||||
operators={operators}
|
||||
onChange={(newOperator: PropertyOperator) => {
|
||||
const tentativeValidationError =
|
||||
newOperator && value ? getValidationError(newOperator, value, propertyKey) : null
|
||||
if (tentativeValidationError) {
|
||||
setValidationError(tentativeValidationError)
|
||||
return
|
||||
}
|
||||
setValidationError(null)
|
||||
|
||||
setCurrentOperator(newOperator)
|
||||
if (isOperatorCohort(newOperator)) {
|
||||
onChange(newOperator, value || null)
|
||||
} else if (isOperatorFlag(newOperator)) {
|
||||
onChange(newOperator, newOperator)
|
||||
} else if (isOperatorFlag(currentOperator || PropertyOperator.Exact)) {
|
||||
onChange(newOperator, null)
|
||||
} else if (
|
||||
isOperatorMulti(currentOperator || PropertyOperator.Exact) &&
|
||||
!isOperatorMulti(newOperator) &&
|
||||
Array.isArray(value)
|
||||
) {
|
||||
onChange(newOperator, value[0])
|
||||
} else if (value) {
|
||||
onChange(newOperator, value)
|
||||
}
|
||||
}}
|
||||
{...operatorSelectProps}
|
||||
size={size}
|
||||
defaultOpen={defaultOpen}
|
||||
/>
|
||||
setCurrentOperator(newOperator)
|
||||
if (isOperatorCohort(newOperator)) {
|
||||
onChange(newOperator, value || null)
|
||||
} else if (isOperatorFlag(newOperator)) {
|
||||
onChange(newOperator, newOperator)
|
||||
} else if (isOperatorFlag(currentOperator || PropertyOperator.Exact)) {
|
||||
onChange(newOperator, null)
|
||||
} else if (
|
||||
isOperatorMulti(currentOperator || PropertyOperator.Exact) &&
|
||||
!isOperatorMulti(newOperator) &&
|
||||
Array.isArray(value)
|
||||
) {
|
||||
onChange(newOperator, value[0])
|
||||
} else if (value) {
|
||||
onChange(newOperator, value)
|
||||
}
|
||||
}}
|
||||
{...operatorSelectProps}
|
||||
size={size}
|
||||
defaultOpen={defaultOpen}
|
||||
/>
|
||||
) : (
|
||||
<span>{allOperatorsToHumanName(currentOperator)} </span>
|
||||
)}
|
||||
</div>
|
||||
{!isOperatorFlag(currentOperator || PropertyOperator.Exact) && type && propertyKey && (
|
||||
<div
|
||||
@@ -201,6 +211,7 @@ export function OperatorValueSelect({
|
||||
autoFocus={!isMobile() && value === null}
|
||||
addRelativeDateTimeOptions={addRelativeDateTimeOptions}
|
||||
groupTypeIndex={groupTypeIndex}
|
||||
editable={editable}
|
||||
size={size}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -4,22 +4,19 @@ import { dayjs } from 'lib/dayjs'
|
||||
import { isOperatorDate } from 'lib/utils'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { PropertyOperator } from '~/types'
|
||||
import { PropertyFilterValue, PropertyOperator } from '~/types'
|
||||
|
||||
const dayJSMightParse = (
|
||||
candidateDateTimeValue: string | number | bigint | (string | number | bigint)[] | null | undefined
|
||||
): candidateDateTimeValue is string | number | undefined => ['string', 'number'].includes(typeof candidateDateTimeValue)
|
||||
const dayJSMightParse = (candidateDateTimeValue: PropertyFilterValue): candidateDateTimeValue is string | number =>
|
||||
['string', 'number'].includes(typeof candidateDateTimeValue)
|
||||
|
||||
const narrowToString = (
|
||||
candidateDateTimeValue: string | number | bigint | (string | number | bigint)[] | null | undefined
|
||||
): candidateDateTimeValue is string | null | undefined =>
|
||||
candidateDateTimeValue == undefined || typeof candidateDateTimeValue === 'string'
|
||||
const narrowToString = (candidateDateTimeValue?: PropertyFilterValue): candidateDateTimeValue is string | null =>
|
||||
typeof candidateDateTimeValue === 'string'
|
||||
|
||||
interface PropertyFilterDatePickerProps {
|
||||
autoFocus: boolean
|
||||
operator: PropertyOperator
|
||||
setValue: (newValue: PropertyValueProps['value']) => void
|
||||
value: string | number | bigint | (string | number | bigint)[] | null | undefined
|
||||
value: string | number | null
|
||||
}
|
||||
|
||||
const dateAndTimeFormat = 'YYYY-MM-DD HH:mm:ss'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LemonButtonProps } from '@posthog/lemon-ui'
|
||||
import { LemonButton, LemonButtonProps } from '@posthog/lemon-ui'
|
||||
import { useActions, useValues } from 'kea'
|
||||
import { DateFilter } from 'lib/components/DateFilter/DateFilter'
|
||||
import { DurationPicker } from 'lib/components/DurationPicker/DurationPicker'
|
||||
@@ -8,13 +8,20 @@ import { dayjs } from 'lib/dayjs'
|
||||
import { LemonInputSelect } from 'lib/lemon-ui/LemonInputSelect/LemonInputSelect'
|
||||
import { formatDate, isOperatorDate, isOperatorFlag, isOperatorMulti, toString } from 'lib/utils'
|
||||
import { useEffect } from 'react'
|
||||
import {
|
||||
AssigneeIconDisplay,
|
||||
AssigneeLabelDisplay,
|
||||
AssigneeResolver,
|
||||
} from 'scenes/error-tracking/components/Assignee/AssigneeDisplay'
|
||||
import { AssigneeSelect } from 'scenes/error-tracking/components/Assignee/AssigneeSelect'
|
||||
|
||||
import {
|
||||
PROPERTY_FILTER_TYPES_WITH_ALL_TIME_SUGGESTIONS,
|
||||
PROPERTY_FILTER_TYPES_WITH_TEMPORAL_SUGGESTIONS,
|
||||
propertyDefinitionsModel,
|
||||
} from '~/models/propertyDefinitionsModel'
|
||||
import { GroupTypeIndex, PropertyFilterType, PropertyOperator, PropertyType } from '~/types'
|
||||
import { ErrorTrackingIssueAssignee } from '~/queries/schema/schema-general'
|
||||
import { GroupTypeIndex, PropertyFilterType, PropertyFilterValue, PropertyOperator, PropertyType } from '~/types'
|
||||
|
||||
export interface PropertyValueProps {
|
||||
propertyKey: string
|
||||
@@ -22,16 +29,16 @@ export interface PropertyValueProps {
|
||||
endpoint?: string // Endpoint to fetch options from
|
||||
placeholder?: string
|
||||
onSet: CallableFunction
|
||||
value?: string | number | bigint | Array<string | number | bigint> | null // | ErrorTrackingIssueAssignee TODO - @david
|
||||
value?: PropertyFilterValue
|
||||
operator: PropertyOperator
|
||||
autoFocus?: boolean
|
||||
eventNames?: string[]
|
||||
addRelativeDateTimeOptions?: boolean
|
||||
forceSingleSelect?: boolean
|
||||
inputClassName?: string
|
||||
additionalPropertiesFilter?: { key: string; values: string | string[] }[]
|
||||
groupTypeIndex?: GroupTypeIndex
|
||||
size?: LemonButtonProps['size']
|
||||
editable?: boolean
|
||||
}
|
||||
|
||||
export function PropertyValue({
|
||||
@@ -46,24 +53,23 @@ export function PropertyValue({
|
||||
autoFocus = false,
|
||||
eventNames = [],
|
||||
addRelativeDateTimeOptions = false,
|
||||
forceSingleSelect = false,
|
||||
inputClassName = undefined,
|
||||
additionalPropertiesFilter = [],
|
||||
groupTypeIndex = undefined,
|
||||
editable = true,
|
||||
}: PropertyValueProps): JSX.Element {
|
||||
const { formatPropertyValueForDisplay, describeProperty, options } = useValues(propertyDefinitionsModel)
|
||||
const { loadPropertyValues } = useActions(propertyDefinitionsModel)
|
||||
|
||||
const isMultiSelect = operator && isOperatorMulti(operator) && !forceSingleSelect
|
||||
const isMultiSelect = operator && isOperatorMulti(operator)
|
||||
const isDateTimeProperty = operator && isOperatorDate(operator)
|
||||
const propertyDefinitionType = propertyFilterTypeToPropertyDefinitionType(type)
|
||||
|
||||
const isDurationProperty =
|
||||
propertyKey && describeProperty(propertyKey, propertyDefinitionType) === PropertyType.Duration
|
||||
|
||||
// TODO - @david
|
||||
// const isAssigneeProperty =
|
||||
// propertyKey && describeProperty(propertyKey, propertyDefinitionType) === PropertyType.Assignee
|
||||
const isAssigneeProperty =
|
||||
propertyKey && describeProperty(propertyKey, propertyDefinitionType) === PropertyType.Assignee
|
||||
|
||||
const load = (newInput: string | undefined): void => {
|
||||
loadPropertyValues({
|
||||
@@ -92,20 +98,34 @@ export function PropertyValue({
|
||||
}
|
||||
}
|
||||
|
||||
// TODO - @david
|
||||
// if (isAssigneeProperty) {
|
||||
// return (
|
||||
// <AssigneeSelect
|
||||
// showName
|
||||
// type="secondary"
|
||||
// fullWidth
|
||||
// allowRemoval={false}
|
||||
// size={size}
|
||||
// assignee={value as ErrorTrackingIssueAssignee}
|
||||
// onChange={setValue}
|
||||
// />
|
||||
// )
|
||||
// }
|
||||
if (isAssigneeProperty) {
|
||||
return editable ? (
|
||||
<AssigneeSelect assignee={value as ErrorTrackingIssueAssignee} onChange={setValue}>
|
||||
{(displayAssignee) => (
|
||||
<LemonButton fullWidth type="secondary" size={size}>
|
||||
<AssigneeLabelDisplay assignee={displayAssignee} placeholder="Choose user" />
|
||||
</LemonButton>
|
||||
)}
|
||||
</AssigneeSelect>
|
||||
) : (
|
||||
<AssigneeResolver assignee={value as ErrorTrackingIssueAssignee}>
|
||||
{({ assignee }) => (
|
||||
<>
|
||||
<AssigneeIconDisplay assignee={assignee} />
|
||||
<AssigneeLabelDisplay assignee={assignee} />
|
||||
</>
|
||||
)}
|
||||
</AssigneeResolver>
|
||||
)
|
||||
}
|
||||
|
||||
const formattedValues = (value === null || value === undefined ? [] : Array.isArray(value) ? value : [value]).map(
|
||||
(label) => String(formatPropertyValueForDisplay(propertyKey, label, propertyDefinitionType, groupTypeIndex))
|
||||
)
|
||||
|
||||
if (!editable) {
|
||||
return <>{formattedValues.join(' or ')}</>
|
||||
}
|
||||
|
||||
if (isDurationProperty) {
|
||||
return <DurationPicker autoFocus={autoFocus} value={value as number} onChange={setValue} />
|
||||
@@ -114,7 +134,12 @@ export function PropertyValue({
|
||||
if (isDateTimeProperty) {
|
||||
if (!addRelativeDateTimeOptions || operator === PropertyOperator.IsDateExact) {
|
||||
return (
|
||||
<PropertyFilterDatePicker autoFocus={autoFocus} operator={operator} value={value} setValue={setValue} />
|
||||
<PropertyFilterDatePicker
|
||||
autoFocus={autoFocus}
|
||||
operator={operator}
|
||||
value={value as string | number | null}
|
||||
setValue={setValue}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -155,10 +180,6 @@ export function PropertyValue({
|
||||
)
|
||||
}
|
||||
|
||||
const formattedValues = (value === null || value === undefined ? [] : Array.isArray(value) ? value : [value]).map(
|
||||
(label) => String(formatPropertyValueForDisplay(propertyKey, label, propertyDefinitionType, groupTypeIndex))
|
||||
)
|
||||
|
||||
return (
|
||||
<LemonInputSelect
|
||||
className={inputClassName}
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
height: 40px; // Matches typical row height
|
||||
|
||||
.TaxonomicPropertyFilter__row--or-filtering & {
|
||||
width: 2rem;
|
||||
width: 2.125rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
gap: 0.25rem;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -56,7 +56,11 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.TaxonomicPropertyFilter__row--showing-operators & {
|
||||
.TaxonomicPropertyFilter__row--editable & {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.TaxonomicPropertyFilter__row--showing-operators.TaxonomicPropertyFilter__row--editable & {
|
||||
> :first-child {
|
||||
flex-grow: 1;
|
||||
width: 30%;
|
||||
|
||||
@@ -59,6 +59,7 @@ export function TaxonomicPropertyFilter({
|
||||
exactMatchFeatureFlagCohortOperators,
|
||||
hideBehavioralCohorts,
|
||||
addFilterDocLink,
|
||||
editable = true,
|
||||
}: PropertyFilterInternalProps): JSX.Element {
|
||||
const pageKey = useMemo(() => pageKeyInput || `filter-${uniqueMemoizedIndex++}`, [pageKeyInput])
|
||||
const groupTypes = taxonomicGroupTypes || [
|
||||
@@ -137,6 +138,8 @@ export function TaxonomicPropertyFilter({
|
||||
filter?.type || PropertyDefinitionType.Event,
|
||||
isGroupPropertyFilter(filter) ? filter?.group_type_index : undefined
|
||||
)}
|
||||
size={size}
|
||||
editable={editable}
|
||||
type={filter?.type}
|
||||
propertyKey={filter?.key}
|
||||
operator={isPropertyFilterWithOperator(filter) ? filter.operator : null}
|
||||
@@ -168,6 +171,20 @@ export function TaxonomicPropertyFilter({
|
||||
/>
|
||||
)
|
||||
|
||||
const filterContent =
|
||||
filter?.type === 'cohort'
|
||||
? filter.cohort_name || `Cohort #${filter?.value}`
|
||||
: filter?.type === PropertyFilterType.EventMetadata && filter?.key?.startsWith('$group_')
|
||||
? filter.label || `Group ${filter?.value}`
|
||||
: filter?.key && (
|
||||
<PropertyKeyInfo
|
||||
value={filter.key}
|
||||
disablePopover
|
||||
ellipsis
|
||||
type={PROPERTY_FILTER_TYPE_TO_TAXONOMIC_FILTER_GROUP_TYPE[filter.type]}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx('TaxonomicPropertyFilter', {
|
||||
@@ -181,6 +198,7 @@ export function TaxonomicPropertyFilter({
|
||||
className={clsx('TaxonomicPropertyFilter__row', {
|
||||
'TaxonomicPropertyFilter__row--or-filtering': orFiltering,
|
||||
'TaxonomicPropertyFilter__row--showing-operators': showOperatorValueSelect,
|
||||
'TaxonomicPropertyFilter__row--editable': editable,
|
||||
})}
|
||||
>
|
||||
{hasRowOperator && (
|
||||
@@ -188,8 +206,12 @@ export function TaxonomicPropertyFilter({
|
||||
{orFiltering ? (
|
||||
<>
|
||||
{propertyGroupType && index !== 0 && filter?.key && (
|
||||
<div className="text-sm font-medium">
|
||||
{propertyGroupType === FilterLogicalOperator.And ? '&' : propertyGroupType}
|
||||
<div className="flex items-center">
|
||||
{propertyGroupType === FilterLogicalOperator.And ? (
|
||||
<OperandTag operand="and" />
|
||||
) : (
|
||||
<OperandTag operand="or" />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
@@ -209,38 +231,28 @@ export function TaxonomicPropertyFilter({
|
||||
)}
|
||||
<div className="TaxonomicPropertyFilter__row-items">
|
||||
{showOperatorValueSelect && placeOperatorValueSelectOnLeft && operatorValueSelect}
|
||||
<LemonDropdown
|
||||
overlay={taxonomicFilter}
|
||||
placement="bottom-start"
|
||||
visible={dropdownOpen}
|
||||
onClickOutside={closeDropdown}
|
||||
>
|
||||
<LemonButton
|
||||
type="secondary"
|
||||
icon={!valuePresent ? <IconPlusSmall /> : undefined}
|
||||
data-attr={'property-select-toggle-' + index}
|
||||
sideIcon={null} // The null sideIcon is here on purpose - it prevents the dropdown caret
|
||||
onClick={() => (dropdownOpen ? closeDropdown() : openDropdown())}
|
||||
size={size}
|
||||
tooltipDocLink={addFilterDocLink}
|
||||
{editable ? (
|
||||
<LemonDropdown
|
||||
overlay={taxonomicFilter}
|
||||
placement="bottom-start"
|
||||
visible={dropdownOpen}
|
||||
onClickOutside={closeDropdown}
|
||||
>
|
||||
{filter?.type === 'cohort' ? (
|
||||
filter.cohort_name || `Cohort #${filter?.value}`
|
||||
) : filter?.type === PropertyFilterType.EventMetadata &&
|
||||
filter?.key?.startsWith('$group_') ? (
|
||||
filter.label || `Group ${filter?.value}`
|
||||
) : filter?.key ? (
|
||||
<PropertyKeyInfo
|
||||
value={filter.key}
|
||||
disablePopover
|
||||
ellipsis
|
||||
type={PROPERTY_FILTER_TYPE_TO_TAXONOMIC_FILTER_GROUP_TYPE[filter.type]}
|
||||
/>
|
||||
) : (
|
||||
addText || 'Add filter'
|
||||
)}
|
||||
</LemonButton>
|
||||
</LemonDropdown>
|
||||
<LemonButton
|
||||
type="secondary"
|
||||
icon={!valuePresent ? <IconPlusSmall /> : undefined}
|
||||
data-attr={'property-select-toggle-' + index}
|
||||
sideIcon={null} // The null sideIcon is here on purpose - it prevents the dropdown caret
|
||||
onClick={() => (dropdownOpen ? closeDropdown() : openDropdown())}
|
||||
size={size}
|
||||
tooltipDocLink={addFilterDocLink}
|
||||
>
|
||||
{filterContent ?? (addText || 'Add filter')}
|
||||
</LemonButton>
|
||||
</LemonDropdown>
|
||||
) : (
|
||||
filterContent
|
||||
)}
|
||||
{showOperatorValueSelect && !placeOperatorValueSelectOnLeft && operatorValueSelect}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -42,6 +42,7 @@ export interface PropertyFilterInternalProps {
|
||||
disablePopover: boolean
|
||||
filters: AnyPropertyFilter[]
|
||||
setFilter: (index: number, property: AnyPropertyFilter) => void
|
||||
editable?: boolean
|
||||
taxonomicGroupTypes?: TaxonomicFilterGroupType[]
|
||||
taxonomicFilterOptionsFromProp?: TaxonomicFilterProps['optionsFromProp']
|
||||
eventNames?: string[]
|
||||
|
||||
@@ -115,7 +115,6 @@ export const PROPERTY_FILTER_TYPE_TO_TAXONOMIC_FILTER_GROUP_TYPE: Record<Propert
|
||||
[PropertyFilterType.Recording]: TaxonomicFilterGroupType.Replay,
|
||||
[PropertyFilterType.LogEntry]: TaxonomicFilterGroupType.LogEntries,
|
||||
[PropertyFilterType.ErrorTrackingIssue]: TaxonomicFilterGroupType.ErrorTrackingIssues,
|
||||
[PropertyFilterType.ErrorTrackingIssueProperty]: TaxonomicFilterGroupType.ErrorTrackingIssueProperties,
|
||||
}
|
||||
|
||||
export function formatPropertyLabel(
|
||||
@@ -357,9 +356,9 @@ export function propertyFilterTypeToPropertyDefinitionType(
|
||||
? PropertyDefinitionType.Session
|
||||
: filterType === PropertyFilterType.LogEntry
|
||||
? PropertyDefinitionType.LogEntry
|
||||
: // : filterType === PropertyFilterType.ErrorTrackingIssue - TODO - @david
|
||||
// ? PropertyDefinitionType.Resource
|
||||
PropertyDefinitionType.Event
|
||||
: filterType === PropertyFilterType.ErrorTrackingIssue
|
||||
? PropertyDefinitionType.Resource
|
||||
: PropertyDefinitionType.Event
|
||||
}
|
||||
|
||||
export function taxonomicFilterTypeToPropertyFilterType(
|
||||
@@ -391,10 +390,6 @@ export function taxonomicFilterTypeToPropertyFilterType(
|
||||
return PropertyFilterType.DataWarehousePersonProperty
|
||||
}
|
||||
|
||||
if (filterType == TaxonomicFilterGroupType.ErrorTrackingIssueProperties) {
|
||||
return PropertyFilterType.ErrorTrackingIssueProperty
|
||||
}
|
||||
|
||||
if (filterType == TaxonomicFilterGroupType.ErrorTrackingIssues) {
|
||||
return PropertyFilterType.ErrorTrackingIssue
|
||||
}
|
||||
|
||||
@@ -381,28 +381,19 @@ export const taxonomicFilterLogic = kea<taxonomicFilterLogicType>([
|
||||
type: TaxonomicFilterGroupType.ErrorTrackingIssues,
|
||||
options: Object.entries(
|
||||
CORE_FILTER_DEFINITIONS_BY_GROUP[TaxonomicFilterGroupType.ErrorTrackingIssues]
|
||||
).map(([key, { label }]) => ({
|
||||
value: key,
|
||||
name: label,
|
||||
})),
|
||||
)
|
||||
.map(([key, { label }]) => ({
|
||||
value: key,
|
||||
name: label,
|
||||
}))
|
||||
.filter(
|
||||
(o) =>
|
||||
!excludedProperties[TaxonomicFilterGroupType.ErrorTrackingIssues]?.includes(o.value)
|
||||
),
|
||||
getName: (option) => option.name,
|
||||
getValue: (option) => option.value,
|
||||
getPopoverHeader: () => 'Issues',
|
||||
},
|
||||
{
|
||||
name: 'Issue properties',
|
||||
searchPlaceholder: 'issue properties',
|
||||
type: TaxonomicFilterGroupType.ErrorTrackingIssueProperties,
|
||||
options: Object.entries(
|
||||
CORE_FILTER_DEFINITIONS_BY_GROUP[TaxonomicFilterGroupType.ErrorTrackingIssueProperties]
|
||||
).map(([key, { label }]) => ({
|
||||
value: key,
|
||||
name: label,
|
||||
})),
|
||||
getName: (option) => option.name,
|
||||
getValue: (option) => option.value,
|
||||
getPopoverHeader: () => 'Issue properties',
|
||||
},
|
||||
{
|
||||
name: 'Numerical event properties',
|
||||
searchPlaceholder: 'numerical event properties',
|
||||
|
||||
@@ -138,9 +138,9 @@ export enum TaxonomicFilterGroupType {
|
||||
Notebooks = 'notebooks',
|
||||
LogEntries = 'log_entries',
|
||||
ErrorTrackingIssues = 'error_tracking_issues',
|
||||
ErrorTrackingIssueProperties = 'error_tracking_issue_properties',
|
||||
// Misc
|
||||
Replay = 'replay',
|
||||
Resources = 'resources',
|
||||
}
|
||||
|
||||
export interface InfiniteListLogicProps extends TaxonomicFilterLogicProps {
|
||||
|
||||
@@ -253,16 +253,23 @@ export const cohortOperatorMap: Record<string, string> = {
|
||||
}
|
||||
|
||||
export const stickinessOperatorMap: Record<string, string> = {
|
||||
exact: 'Exactly',
|
||||
gte: 'At least',
|
||||
lte: 'At most (but at least once)',
|
||||
exact: '= Exactly',
|
||||
gte: '≥ At least',
|
||||
lte: '≤ At most (but at least once)',
|
||||
}
|
||||
|
||||
export const cleanedPathOperatorMap: Record<string, string> = {
|
||||
is_cleaned_path_exact: '= equals',
|
||||
}
|
||||
|
||||
export const assigneeOperatorMap: Record<string, string> = {
|
||||
exact: '= is',
|
||||
is_not: '≠ is not',
|
||||
is_not_set: '✕ is not set',
|
||||
}
|
||||
|
||||
export const allOperatorsMapping: Record<string, string> = {
|
||||
...assigneeOperatorMap,
|
||||
...stickinessOperatorMap,
|
||||
...dateTimeOperatorMap,
|
||||
...stringOperatorMap,
|
||||
@@ -285,6 +292,7 @@ const operatorMappingChoice: Record<keyof typeof PropertyType, Record<string, st
|
||||
Duration: durationOperatorMap,
|
||||
Selector: selectorOperatorMap,
|
||||
Cohort: cohortOperatorMap,
|
||||
Assignee: assigneeOperatorMap,
|
||||
}
|
||||
|
||||
export function chooseOperatorMap(propertyType: PropertyType | undefined): Record<string, string> {
|
||||
|
||||
@@ -237,7 +237,10 @@ describe('the property definitions model', () => {
|
||||
'event_metadata/distinct_id': partial({ name: 'distinct_id' }),
|
||||
'event_metadata/event': partial({ name: 'event' }),
|
||||
'event_metadata/person_id': partial({ name: 'person_id' }),
|
||||
'event_metadata/timestamp': partial({ name: 'timestamp' }),
|
||||
'event_metadata/timestamp': partial({
|
||||
name: 'timestamp',
|
||||
}),
|
||||
'resource/assignee': partial({ name: 'assignee' }),
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -53,13 +53,12 @@ const localProperties: PropertyDefinitionStorage = {
|
||||
is_seen_on_filtered_events: false,
|
||||
property_type: PropertyType.Selector,
|
||||
},
|
||||
// TODO - @david
|
||||
// 'resource/assignee': {
|
||||
// id: 'assignee',
|
||||
// name: 'assignee',
|
||||
// description: 'User or role the resource is assigned to',
|
||||
// property_type: PropertyType.Assignee,
|
||||
// },
|
||||
'resource/assignee': {
|
||||
id: 'assignee',
|
||||
name: 'assignee',
|
||||
description: 'User or role assigned to a resource',
|
||||
property_type: PropertyType.Assignee,
|
||||
},
|
||||
}
|
||||
|
||||
const localOptions: Record<string, PropValue[]> = {
|
||||
|
||||
@@ -523,9 +523,6 @@
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/ErrorTrackingIssueFilter"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/ErrorTrackingIssuePropertyFilter"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -7809,29 +7806,6 @@
|
||||
"required": ["key", "operator", "type"],
|
||||
"type": "object"
|
||||
},
|
||||
"ErrorTrackingIssuePropertyFilter": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"operator": {
|
||||
"$ref": "#/definitions/PropertyOperator"
|
||||
},
|
||||
"type": {
|
||||
"const": "error_tracking_issue_property",
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"$ref": "#/definitions/PropertyFilterValue"
|
||||
}
|
||||
},
|
||||
"required": ["key", "operator", "type"],
|
||||
"type": "object"
|
||||
},
|
||||
"ErrorTrackingQuery": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -12359,6 +12333,19 @@
|
||||
"required": ["kind"],
|
||||
"type": "object"
|
||||
},
|
||||
"PropertyFilterBaseValue": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"$ref": "#/definitions/ErrorTrackingIssueAssignee"
|
||||
}
|
||||
]
|
||||
},
|
||||
"PropertyFilterType": {
|
||||
"enum": [
|
||||
"meta",
|
||||
@@ -12375,22 +12362,18 @@
|
||||
"hogql",
|
||||
"data_warehouse",
|
||||
"data_warehouse_person_property",
|
||||
"error_tracking_issue",
|
||||
"error_tracking_issue_property"
|
||||
"error_tracking_issue"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PropertyFilterValue": {
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
"$ref": "#/definitions/PropertyFilterBaseValue"
|
||||
},
|
||||
{
|
||||
"items": {
|
||||
"type": ["string", "number"]
|
||||
"$ref": "#/definitions/PropertyFilterBaseValue"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
@@ -17513,8 +17496,8 @@
|
||||
"notebooks",
|
||||
"log_entries",
|
||||
"error_tracking_issues",
|
||||
"error_tracking_issue_properties",
|
||||
"replay"
|
||||
"replay",
|
||||
"resources"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
|
||||
@@ -49,18 +49,16 @@ export function ErrorTrackingIssueScene(): JSX.Element {
|
||||
<PageHeader
|
||||
buttons={
|
||||
<div className="flex gap-x-2">
|
||||
{!issueLoading && issue?.status == 'active' && (
|
||||
{!issueLoading && issue?.status === 'active' && (
|
||||
<AssigneeSelect assignee={issue?.assignee} onChange={updateAssignee}>
|
||||
{(displayAssignee) => {
|
||||
return (
|
||||
<LemonButton
|
||||
type="secondary"
|
||||
icon={<AssigneeIconDisplay assignee={displayAssignee} />}
|
||||
>
|
||||
<AssigneeLabelDisplay assignee={displayAssignee} placeholder="Unassigned" />
|
||||
</LemonButton>
|
||||
)
|
||||
}}
|
||||
{(displayAssignee) => (
|
||||
<LemonButton
|
||||
type="secondary"
|
||||
icon={<AssigneeIconDisplay assignee={displayAssignee} />}
|
||||
>
|
||||
<AssigneeLabelDisplay assignee={displayAssignee} placeholder="Unassigned" />
|
||||
</LemonButton>
|
||||
)}
|
||||
</AssigneeSelect>
|
||||
)}
|
||||
{!issueLoading && (
|
||||
|
||||
@@ -92,13 +92,11 @@ export const ErrorTrackingListOptions = (): JSX.Element => {
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Assigned to:</span>
|
||||
<AssigneeSelect assignee={assignee} onChange={(assignee) => setAssignee(assignee)}>
|
||||
{(displayAssignee) => {
|
||||
return (
|
||||
<LemonButton type="secondary" size="small">
|
||||
<AssigneeLabelDisplay assignee={displayAssignee} placeholder="Any user" />
|
||||
</LemonButton>
|
||||
)
|
||||
}}
|
||||
{(displayAssignee) => (
|
||||
<LemonButton type="secondary" size="small">
|
||||
<AssigneeLabelDisplay assignee={displayAssignee} placeholder="Any user" />
|
||||
</LemonButton>
|
||||
)}
|
||||
</AssigneeSelect>
|
||||
</div>
|
||||
</span>
|
||||
|
||||
@@ -172,22 +172,20 @@ const CustomGroupTitleColumn: QueryContextColumnComponent = (props) => {
|
||||
assignee={record.assignee}
|
||||
onChange={(assignee) => assignIssue(record.id, assignee)}
|
||||
>
|
||||
{(anyAssignee) => {
|
||||
return (
|
||||
<div
|
||||
className="flex items-center hover:bg-fill-button-tertiary-hover p-[0.1rem] rounded cursor-pointer"
|
||||
role="button"
|
||||
>
|
||||
<AssigneeIconDisplay assignee={anyAssignee} size="xsmall" />
|
||||
<AssigneeLabelDisplay
|
||||
assignee={anyAssignee}
|
||||
className="ml-1 text-xs text-secondary"
|
||||
size="xsmall"
|
||||
/>
|
||||
<IconChevronDown />
|
||||
</div>
|
||||
)
|
||||
}}
|
||||
{(anyAssignee) => (
|
||||
<div
|
||||
className="flex items-center hover:bg-fill-button-tertiary-hover p-[0.1rem] rounded cursor-pointer"
|
||||
role="button"
|
||||
>
|
||||
<AssigneeIconDisplay assignee={anyAssignee} size="xsmall" />
|
||||
<AssigneeLabelDisplay
|
||||
assignee={anyAssignee}
|
||||
className="ml-1 text-xs text-secondary"
|
||||
size="xsmall"
|
||||
/>
|
||||
<IconChevronDown />
|
||||
</div>
|
||||
)}
|
||||
</AssigneeSelect>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,64 +1,202 @@
|
||||
import { LemonButton, LemonCard, LemonDivider, LemonSelect } from '@posthog/lemon-ui'
|
||||
import { IconPencil, IconTrash } from '@posthog/icons'
|
||||
import { LemonButton, LemonCard, LemonDialog, LemonDivider, LemonSelect, Spinner } from '@posthog/lemon-ui'
|
||||
import { useActions, useValues } from 'kea'
|
||||
import { PropertyFilters } from 'lib/components/PropertyFilters/PropertyFilters'
|
||||
import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types'
|
||||
import { useEffect } from 'react'
|
||||
import {
|
||||
AssigneeIconDisplay,
|
||||
AssigneeLabelDisplay,
|
||||
AssigneeResolver,
|
||||
} from 'scenes/error-tracking/components/Assignee/AssigneeDisplay'
|
||||
import { AssigneeSelect } from 'scenes/error-tracking/components/Assignee/AssigneeSelect'
|
||||
|
||||
import { ErrorTrackingIssue } from '~/queries/schema/schema-general'
|
||||
import { AnyPropertyFilter, FilterLogicalOperator } from '~/types'
|
||||
|
||||
import { errorTrackingAutoAssignmentLogic } from './errorTrackingAutoAssignmentLogic'
|
||||
import { type ErrorTrackingAssignmentRule, errorTrackingAutoAssignmentLogic } from './errorTrackingAutoAssignmentLogic'
|
||||
|
||||
export function ErrorTrackingAutoAssignment(): JSX.Element {
|
||||
const { assignmentRules, hasNewRule } = useValues(errorTrackingAutoAssignmentLogic)
|
||||
const { loadRules, addRule, updateRule } = useActions(errorTrackingAutoAssignmentLogic)
|
||||
const logic = errorTrackingAutoAssignmentLogic({ startWithNewEditableRule: true })
|
||||
const { allRules, initialLoadComplete, localRules, hasNewRule } = useValues(logic)
|
||||
const { loadRules, addRule, updateLocalRule, deleteRule, saveRule, setRuleEditable, unsetRuleEditable } =
|
||||
useActions(logic)
|
||||
|
||||
useEffect(() => {
|
||||
loadRules()
|
||||
}, [loadRules])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
{assignmentRules.map((rule) => (
|
||||
<LemonCard key={rule.id} hoverEffect={false} className="flex flex-col p-0">
|
||||
<div className="flex gap-2 items-center px-2 py-3">
|
||||
<div>Assign to</div>
|
||||
{/* <AssigneeSelect -- TODO @david
|
||||
assignee={rule.assignee}
|
||||
onChange={(assignee) => updateRule({ ...rule, assignee })}
|
||||
/> */}
|
||||
<div>when</div>
|
||||
<LemonSelect
|
||||
size="small"
|
||||
value={rule.filters.type}
|
||||
onChange={(type) => updateRule({ ...rule, filters: { ...rule.filters, type } })}
|
||||
options={[
|
||||
{ label: 'All', value: FilterLogicalOperator.And },
|
||||
{ label: 'Any', value: FilterLogicalOperator.Or },
|
||||
]}
|
||||
/>
|
||||
<div>filters match</div>
|
||||
</div>
|
||||
<LemonDivider className="my-0" />
|
||||
<div className="py-2">
|
||||
<PropertyFilters
|
||||
propertyFilters={(rule.filters.values as AnyPropertyFilter[]) ?? []}
|
||||
taxonomicGroupTypes={[TaxonomicFilterGroupType.ErrorTrackingIssueProperties]}
|
||||
onChange={(properties: AnyPropertyFilter[]) =>
|
||||
updateRule({ ...rule, filters: { ...rule.filters, values: properties } })
|
||||
}
|
||||
pageKey={`error-tracking-auto-assignment-properties-${rule.id}`}
|
||||
buttonSize="small"
|
||||
disablePopover
|
||||
/>
|
||||
</div>
|
||||
</LemonCard>
|
||||
))}
|
||||
if (!initialLoadComplete) {
|
||||
return <Spinner />
|
||||
}
|
||||
|
||||
<div>
|
||||
<LemonButton type="secondary" size="small" onClick={addRule}>
|
||||
{`${hasNewRule ? 'Save' : 'Add'} rule`}
|
||||
</LemonButton>
|
||||
</div>
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2 mt-2">
|
||||
{allRules.map((persistedRule) => {
|
||||
const editingRule = localRules.find((r) => r.id === persistedRule.id)
|
||||
|
||||
const editable = !!editingRule
|
||||
const rule = editingRule ?? persistedRule
|
||||
|
||||
return (
|
||||
<LemonCard key={rule.id} hoverEffect={false} className="flex flex-col p-0">
|
||||
<div className="flex gap-2 justify-between px-2 py-3">
|
||||
<div className="flex gap-1 items-center">
|
||||
<div>Assign to</div>
|
||||
<RuleAssignee
|
||||
assignee={rule.assignee}
|
||||
onChange={(assignee) => updateLocalRule({ ...rule, assignee })}
|
||||
editable={editable}
|
||||
/>
|
||||
<div>when</div>
|
||||
<RuleOperator
|
||||
operator={rule.filters.type}
|
||||
onChange={(type) =>
|
||||
updateLocalRule({ ...rule, filters: { ...rule.filters, type } })
|
||||
}
|
||||
editable={editable}
|
||||
/>
|
||||
<div>filters match</div>
|
||||
</div>
|
||||
<RuleActions
|
||||
onClickSave={() => saveRule(rule.id)}
|
||||
onClickDelete={
|
||||
rule.id === 'new'
|
||||
? undefined
|
||||
: () =>
|
||||
LemonDialog.open({
|
||||
title: 'Delete rule',
|
||||
description: 'Are you sure you want to delete your assignment rule?',
|
||||
primaryButton: {
|
||||
status: 'danger',
|
||||
children: 'Remove',
|
||||
onClick: () => deleteRule(rule.id),
|
||||
},
|
||||
secondaryButton: {
|
||||
children: 'Cancel',
|
||||
},
|
||||
})
|
||||
}
|
||||
onClickEdit={() => setRuleEditable(rule.id)}
|
||||
onClickCancel={() => unsetRuleEditable(rule.id)}
|
||||
editable={editable}
|
||||
/>
|
||||
</div>
|
||||
<LemonDivider className="my-0" />
|
||||
<div className="p-2">
|
||||
<PropertyFilters
|
||||
editable={editable}
|
||||
propertyFilters={(rule.filters.values as AnyPropertyFilter[]) ?? []}
|
||||
taxonomicGroupTypes={[TaxonomicFilterGroupType.ErrorTrackingIssues]}
|
||||
onChange={(properties: AnyPropertyFilter[]) =>
|
||||
updateLocalRule({ ...rule, filters: { ...rule.filters, values: properties } })
|
||||
}
|
||||
pageKey={`error-tracking-auto-assignment-properties-${rule.id}`}
|
||||
buttonSize="small"
|
||||
excludedProperties={{ [TaxonomicFilterGroupType.ErrorTrackingIssues]: ['assignee'] }}
|
||||
propertyGroupType={rule.filters.type}
|
||||
hasRowOperator={false}
|
||||
disablePopover
|
||||
/>
|
||||
</div>
|
||||
</LemonCard>
|
||||
)
|
||||
})}
|
||||
|
||||
{!hasNewRule && (
|
||||
<div>
|
||||
<LemonButton type="primary" size="small" onClick={addRule}>
|
||||
Add rule
|
||||
</LemonButton>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const RuleAssignee = ({
|
||||
assignee,
|
||||
editable,
|
||||
onChange,
|
||||
}: {
|
||||
assignee: ErrorTrackingAssignmentRule['assignee']
|
||||
editable: boolean
|
||||
onChange: (assignee: ErrorTrackingIssue['assignee']) => void
|
||||
}): JSX.Element => {
|
||||
return editable ? (
|
||||
<AssigneeSelect assignee={assignee} onChange={onChange}>
|
||||
{(displayAssignee) => (
|
||||
<LemonButton fullWidth type="secondary" size="small">
|
||||
<AssigneeLabelDisplay assignee={displayAssignee} placeholder="Choose user" />
|
||||
</LemonButton>
|
||||
)}
|
||||
</AssigneeSelect>
|
||||
) : (
|
||||
<AssigneeResolver assignee={assignee}>
|
||||
{({ assignee }) => (
|
||||
<>
|
||||
<AssigneeIconDisplay assignee={assignee} />
|
||||
<AssigneeLabelDisplay assignee={assignee} />
|
||||
</>
|
||||
)}
|
||||
</AssigneeResolver>
|
||||
)
|
||||
}
|
||||
|
||||
const RuleOperator = ({
|
||||
operator,
|
||||
onChange,
|
||||
editable,
|
||||
}: {
|
||||
operator: FilterLogicalOperator
|
||||
onChange: (value: FilterLogicalOperator) => void
|
||||
editable: boolean
|
||||
}): JSX.Element => {
|
||||
return editable ? (
|
||||
<LemonSelect
|
||||
size="small"
|
||||
value={operator}
|
||||
onChange={onChange}
|
||||
options={[
|
||||
{ label: 'All', value: FilterLogicalOperator.And },
|
||||
{ label: 'Any', value: FilterLogicalOperator.Or },
|
||||
]}
|
||||
/>
|
||||
) : (
|
||||
<span className="font-semibold">{operator === FilterLogicalOperator.And ? 'all' : 'any'}</span>
|
||||
)
|
||||
}
|
||||
|
||||
const RuleActions = ({
|
||||
editable,
|
||||
onClickSave,
|
||||
onClickCancel,
|
||||
onClickDelete,
|
||||
onClickEdit,
|
||||
}: {
|
||||
editable: boolean
|
||||
onClickSave: () => void
|
||||
onClickCancel: () => void
|
||||
onClickDelete?: () => void
|
||||
onClickEdit: () => void
|
||||
}): JSX.Element => {
|
||||
return (
|
||||
<div className="flex gap-1">
|
||||
{editable ? (
|
||||
<>
|
||||
{onClickDelete && (
|
||||
<LemonButton size="small" icon={<IconTrash />} status="danger" onClick={onClickDelete} />
|
||||
)}
|
||||
<LemonButton size="small" onClick={onClickCancel}>
|
||||
Cancel
|
||||
</LemonButton>
|
||||
<LemonButton size="small" type="primary" onClick={onClickSave}>
|
||||
Save
|
||||
</LemonButton>
|
||||
</>
|
||||
) : (
|
||||
<LemonButton size="small" icon={<IconPencil />} onClick={onClickEdit} />
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { kea, path, selectors } from 'kea'
|
||||
import { actions, kea, listeners, path, props, reducers, selectors } from 'kea'
|
||||
import { loaders } from 'kea-loaders'
|
||||
import api from 'lib/api'
|
||||
|
||||
@@ -13,59 +13,51 @@ export type ErrorTrackingAssignmentRule = {
|
||||
filters: UniversalFiltersGroup
|
||||
}
|
||||
|
||||
// const validRule = (rule: ErrorTrackingAssignmentRule): boolean => {
|
||||
// return rule.assignee !== null && rule.filters.values.length > 0
|
||||
// }
|
||||
|
||||
export const errorTrackingAutoAssignmentLogic = kea<errorTrackingAutoAssignmentLogicType>([
|
||||
path(['scenes', 'error-tracking', 'errorTrackingAutoAssignmentLogic']),
|
||||
props({} as { startWithNewEditableRule: boolean }),
|
||||
|
||||
actions({
|
||||
addRule: true,
|
||||
setRuleEditable: (id: ErrorTrackingAssignmentRule['id']) => ({ id }),
|
||||
unsetRuleEditable: (id: ErrorTrackingAssignmentRule['id']) => ({ id }),
|
||||
updateLocalRule: (rule: ErrorTrackingAssignmentRule) => ({ rule }),
|
||||
_setLocalRules: (rules: ErrorTrackingAssignmentRule[]) => ({ rules }),
|
||||
}),
|
||||
|
||||
reducers({
|
||||
localRules: [[] as ErrorTrackingAssignmentRule[], { _setLocalRules: (_, { rules }) => rules }],
|
||||
initialLoadComplete: [
|
||||
false,
|
||||
{
|
||||
loadRules: () => false,
|
||||
loadRulesSuccess: () => true,
|
||||
loadRulesFailure: () => true,
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
||||
loaders(({ values }) => ({
|
||||
assignmentRules: [
|
||||
[
|
||||
{
|
||||
id: 'new',
|
||||
assignee: null,
|
||||
filters: { type: FilterLogicalOperator.Or, values: [] },
|
||||
},
|
||||
] as ErrorTrackingAssignmentRule[],
|
||||
[] as ErrorTrackingAssignmentRule[],
|
||||
{
|
||||
loadRules: async () => {
|
||||
const res = await api.errorTracking.assignmentRules()
|
||||
const rules = res.results
|
||||
if (rules.length === 0) {
|
||||
rules.push({
|
||||
id: 'new',
|
||||
assignee: null,
|
||||
filters: { type: FilterLogicalOperator.Or, values: [] },
|
||||
})
|
||||
}
|
||||
const { results: rules } = await api.errorTracking.assignmentRules()
|
||||
return rules
|
||||
},
|
||||
addRule: async () => {
|
||||
return [
|
||||
...values.assignmentRules,
|
||||
{
|
||||
id: 'new',
|
||||
assignee: null,
|
||||
filters: { type: FilterLogicalOperator.Or, values: [] },
|
||||
},
|
||||
]
|
||||
},
|
||||
saveRule: async (rule) => {
|
||||
saveRule: async (id) => {
|
||||
const rule = values.localRules.find((r) => r.id === id)
|
||||
const newValues = [...values.assignmentRules]
|
||||
if (rule.id === 'new') {
|
||||
const newRule = await api.errorTracking.createAssignmentRule(rule)
|
||||
return newValues.map((r) => (rule.id === r.id ? newRule : r))
|
||||
if (rule) {
|
||||
if (rule.id === 'new') {
|
||||
const newRule = await api.errorTracking.createAssignmentRule(rule)
|
||||
return [...newValues, newRule]
|
||||
}
|
||||
await api.errorTracking.updateAssignmentRule(rule)
|
||||
return newValues.map((r) => (r.id === rule.id ? rule : r))
|
||||
}
|
||||
await api.errorTracking.updateAssignmentRule(rule)
|
||||
|
||||
return newValues
|
||||
},
|
||||
updateRule: async (rule) => {
|
||||
const newValues = [...values.assignmentRules]
|
||||
return newValues.map((r) => (rule.id === r.id ? rule : r))
|
||||
},
|
||||
deleteRule: async (id) => {
|
||||
if (id != 'new') {
|
||||
await api.errorTracking.deleteAssignmentRule(id)
|
||||
@@ -77,7 +69,69 @@ export const errorTrackingAutoAssignmentLogic = kea<errorTrackingAutoAssignmentL
|
||||
],
|
||||
})),
|
||||
|
||||
listeners(({ props, values, actions }) => ({
|
||||
loadRulesSuccess: ({ assignmentRules }) => {
|
||||
if (assignmentRules.length === 0 && props.startWithNewEditableRule) {
|
||||
actions._setLocalRules([
|
||||
{
|
||||
id: 'new',
|
||||
assignee: null,
|
||||
filters: { type: FilterLogicalOperator.Or, values: [] },
|
||||
},
|
||||
])
|
||||
}
|
||||
},
|
||||
addRule: () => {
|
||||
actions._setLocalRules([
|
||||
...values.localRules,
|
||||
{
|
||||
id: 'new',
|
||||
assignee: null,
|
||||
filters: { type: FilterLogicalOperator.Or, values: [] },
|
||||
},
|
||||
])
|
||||
},
|
||||
saveRuleSuccess: ({ payload: id }) => {
|
||||
const localRules = [...values.localRules]
|
||||
const newEditingRules = localRules.filter((v) => v.id !== id)
|
||||
actions._setLocalRules(newEditingRules)
|
||||
},
|
||||
deleteRuleSuccess: ({ payload: id }) => {
|
||||
const localRules = [...values.localRules]
|
||||
const newEditingRules = localRules.filter((v) => v.id !== id)
|
||||
actions._setLocalRules(newEditingRules)
|
||||
},
|
||||
setRuleEditable: ({ id }) => {
|
||||
const rule = values.assignmentRules.find((r) => r.id === id)
|
||||
if (rule) {
|
||||
actions._setLocalRules([...values.localRules, rule])
|
||||
}
|
||||
},
|
||||
unsetRuleEditable: ({ id }) => {
|
||||
const newLocalRules = [...values.localRules]
|
||||
const index = newLocalRules.findIndex((r) => r.id === id)
|
||||
if (index >= 0) {
|
||||
newLocalRules.splice(index, 1)
|
||||
actions._setLocalRules(newLocalRules)
|
||||
}
|
||||
},
|
||||
updateLocalRule: ({ rule }) => {
|
||||
const newEditingRules = [...values.localRules]
|
||||
const index = newEditingRules.findIndex((r) => r.id === rule.id)
|
||||
|
||||
if (index >= 0) {
|
||||
newEditingRules.splice(index, 1, rule)
|
||||
actions._setLocalRules(newEditingRules)
|
||||
}
|
||||
},
|
||||
})),
|
||||
|
||||
selectors({
|
||||
hasNewRule: [(s) => [s.assignmentRules], (assignmentRules) => assignmentRules.some(({ id }) => id === 'new')],
|
||||
allRules: [
|
||||
(s) => [s.localRules, s.assignmentRules],
|
||||
(localRules, assignmentRules): ErrorTrackingAssignmentRule[] =>
|
||||
Array.from(new Map([...assignmentRules, ...localRules].map((item) => [item.id, item])).values()),
|
||||
],
|
||||
hasNewRule: [(s) => [s.allRules], (allRules): boolean => allRules.some((r) => r.id === 'new')],
|
||||
}),
|
||||
])
|
||||
|
||||
@@ -95,13 +95,11 @@ export function BulkActions(): JSX.Element {
|
||||
}}
|
||||
/>
|
||||
<AssigneeSelect assignee={null} onChange={(assignee) => assignIssues(selectedIssueIds, assignee)}>
|
||||
{(displayAssignee) => {
|
||||
return (
|
||||
<LemonButton type="secondary" size="small">
|
||||
<AssigneeLabelDisplay assignee={displayAssignee} placeholder="Assign" />
|
||||
</LemonButton>
|
||||
)
|
||||
}}
|
||||
{(displayAssignee) => (
|
||||
<LemonButton type="secondary" size="small">
|
||||
<AssigneeLabelDisplay assignee={displayAssignee} placeholder="Assign" />
|
||||
</LemonButton>
|
||||
)}
|
||||
</AssigneeSelect>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -2,6 +2,7 @@ import { LemonTag, Link, Tooltip } from '@posthog/lemon-ui'
|
||||
import { OrganizationMembershipLevel } from 'lib/constants'
|
||||
import { dayjs } from 'lib/dayjs'
|
||||
import { ErrorTrackingAlerting } from 'scenes/error-tracking/configuration/alerting/ErrorTrackingAlerting'
|
||||
import { ErrorTrackingAutoAssignment } from 'scenes/error-tracking/configuration/auto-assignment/ErrorTrackingAutoAssignment'
|
||||
import { ErrorTrackingSymbolSets } from 'scenes/error-tracking/configuration/symbol-sets/ErrorTrackingSymbolSets'
|
||||
import { organizationLogic } from 'scenes/organizationLogic'
|
||||
import { BounceRateDurationSetting } from 'scenes/settings/environment/BounceRateDuration'
|
||||
@@ -358,6 +359,12 @@ export const SETTINGS_MAP: SettingSection[] = [
|
||||
component: <ErrorTrackingIntegrations />,
|
||||
flag: 'ERROR_TRACKING_INTEGRATIONS',
|
||||
},
|
||||
{
|
||||
id: 'error-tracking-auto-assignment',
|
||||
title: 'Auto assignment',
|
||||
component: <ErrorTrackingAutoAssignment />,
|
||||
flag: 'ERROR_TRACKING_ALERT_ROUTING',
|
||||
},
|
||||
{
|
||||
id: 'error-tracking-symbol-sets',
|
||||
title: 'Symbol sets',
|
||||
|
||||
@@ -55,6 +55,7 @@ import {
|
||||
InsightLogicProps,
|
||||
InsightType,
|
||||
IntervalType,
|
||||
PropertyFilterBaseValue,
|
||||
PropertyFilterType,
|
||||
PropertyMathType,
|
||||
PropertyOperator,
|
||||
@@ -468,7 +469,7 @@ export const webAnalyticsLogic = kea<webAnalyticsLogicType>([
|
||||
if (similarFilterExists) {
|
||||
// if there's already a matching property, turn it off or merge them
|
||||
return oldPropertyFilters
|
||||
.map((f) => {
|
||||
.map((f: WebAnalyticsPropertyFilter) => {
|
||||
if (
|
||||
f.key !== key ||
|
||||
f.type !== type ||
|
||||
@@ -477,7 +478,7 @@ export const webAnalyticsLogic = kea<webAnalyticsLogicType>([
|
||||
return f
|
||||
}
|
||||
const oldValue = (Array.isArray(f.value) ? f.value : [f.value]).filter(isNotNil)
|
||||
let newValue: (string | number | bigint)[]
|
||||
let newValue: PropertyFilterBaseValue[]
|
||||
if (oldValue.includes(value)) {
|
||||
// If there are multiple values for this filter, reduce that to just the one being clicked
|
||||
if (oldValue.length > 1) {
|
||||
|
||||
@@ -3526,22 +3526,16 @@
|
||||
}
|
||||
},
|
||||
"error_tracking_issues": {
|
||||
"name": {
|
||||
"label": "Name",
|
||||
"description": "The name of the issue."
|
||||
},
|
||||
"assignee": {
|
||||
"label": "Assignee",
|
||||
"description": "The current assignee of an issue."
|
||||
}
|
||||
},
|
||||
"error_tracking_issue_properties": {
|
||||
},
|
||||
"exception_type": {
|
||||
"label": "Exception type",
|
||||
"description": "The type of the exception."
|
||||
},
|
||||
"exception_message": {
|
||||
"label": "Exception message",
|
||||
"description": "The message of the exception."
|
||||
},
|
||||
"exception_description": {
|
||||
"label": "Exception description",
|
||||
"description": "The description of the exception."
|
||||
},
|
||||
"exception_function": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types'
|
||||
|
||||
import { CoreFilterDefinition, PropertyFilterValue } from '~/types'
|
||||
import { CoreFilterDefinition } from '~/types'
|
||||
|
||||
import { CORE_FILTER_DEFINITIONS_BY_GROUP } from './taxonomy'
|
||||
|
||||
@@ -13,7 +13,7 @@ export function isCoreFilter(key: string): boolean {
|
||||
export type PropertyKey = string | null | undefined
|
||||
|
||||
export function getCoreFilterDefinition(
|
||||
value: string | PropertyFilterValue | undefined,
|
||||
value: string | null | undefined,
|
||||
type: TaxonomicFilterGroupType
|
||||
): CoreFilterDefinition | null {
|
||||
if (value == undefined) {
|
||||
|
||||
@@ -36,6 +36,7 @@ import { WEB_SAFE_FONTS } from 'scenes/surveys/constants'
|
||||
import type {
|
||||
DashboardFilter,
|
||||
DatabaseSchemaField,
|
||||
ErrorTrackingIssueAssignee,
|
||||
ExperimentExposureCriteria,
|
||||
ExperimentFunnelsQuery,
|
||||
ExperimentMetric,
|
||||
@@ -715,13 +716,8 @@ export interface ToolbarProps extends ToolbarParams {
|
||||
|
||||
export type PathCleaningFilter = { alias?: string; regex?: string }
|
||||
|
||||
export type PropertyFilterValue =
|
||||
| string
|
||||
| number
|
||||
| bigint
|
||||
| (string | number | bigint)[]
|
||||
// | ErrorTrackingIssueAssignee - TODO - @david
|
||||
| null
|
||||
export type PropertyFilterBaseValue = string | number | bigint | ErrorTrackingIssueAssignee
|
||||
export type PropertyFilterValue = PropertyFilterBaseValue | PropertyFilterBaseValue[] | null
|
||||
|
||||
/** Sync with plugin-server/src/types.ts */
|
||||
export enum PropertyOperator {
|
||||
@@ -842,7 +838,6 @@ export enum PropertyFilterType {
|
||||
DataWarehouse = 'data_warehouse',
|
||||
DataWarehousePersonProperty = 'data_warehouse_person_property',
|
||||
ErrorTrackingIssue = 'error_tracking_issue',
|
||||
ErrorTrackingIssueProperty = 'error_tracking_issue_property',
|
||||
}
|
||||
|
||||
/** Sync with plugin-server/src/types.ts */
|
||||
@@ -886,11 +881,6 @@ export interface ErrorTrackingIssueFilter extends BasePropertyFilter {
|
||||
operator: PropertyOperator
|
||||
}
|
||||
|
||||
export interface ErrorTrackingIssuePropertyFilter extends BasePropertyFilter {
|
||||
type: PropertyFilterType.ErrorTrackingIssueProperty
|
||||
operator: PropertyOperator
|
||||
}
|
||||
|
||||
/** Sync with plugin-server/src/types.ts */
|
||||
export interface ElementPropertyFilter extends BasePropertyFilter {
|
||||
type: PropertyFilterType.Element
|
||||
@@ -954,7 +944,6 @@ export type AnyPropertyFilter =
|
||||
| DataWarehousePropertyFilter
|
||||
| DataWarehousePersonPropertyFilter
|
||||
| ErrorTrackingIssueFilter
|
||||
| ErrorTrackingIssuePropertyFilter
|
||||
|
||||
/** Any filter type supported by `property_to_expr(scope="person", ...)`. */
|
||||
export type AnyPersonScopeFilter =
|
||||
@@ -3461,6 +3450,7 @@ export enum PropertyType {
|
||||
Duration = 'Duration',
|
||||
Selector = 'Selector',
|
||||
Cohort = 'Cohort',
|
||||
Assignee = 'Assignee',
|
||||
}
|
||||
|
||||
export enum PropertyDefinitionType {
|
||||
@@ -3471,7 +3461,7 @@ export enum PropertyDefinitionType {
|
||||
Session = 'session',
|
||||
LogEntry = 'log_entry',
|
||||
Meta = 'meta',
|
||||
// Resource = 'resource', - TODO @david
|
||||
Resource = 'resource',
|
||||
}
|
||||
|
||||
export interface PropertyDefinition {
|
||||
|
||||
@@ -47,7 +47,6 @@ from posthog.schema import (
|
||||
DataWarehousePropertyFilter,
|
||||
DataWarehousePersonPropertyFilter,
|
||||
ErrorTrackingIssueFilter,
|
||||
ErrorTrackingIssuePropertyFilter,
|
||||
)
|
||||
from posthog.warehouse.models import DataWarehouseJoin
|
||||
from posthog.utils import get_from_dict_or_attr
|
||||
@@ -302,7 +301,6 @@ def property_to_expr(
|
||||
| DataWarehousePropertyFilter
|
||||
| DataWarehousePersonPropertyFilter
|
||||
| ErrorTrackingIssueFilter
|
||||
| ErrorTrackingIssuePropertyFilter
|
||||
),
|
||||
team: Team,
|
||||
scope: Literal["event", "person", "group", "session", "replay", "replay_entity"] = "event",
|
||||
@@ -676,9 +674,11 @@ def action_to_expr(action: Action, events_alias: Optional[str] = None) -> ast.Ex
|
||||
expr = ast.CompareOperation(
|
||||
op=ast.CompareOperationOp.Eq,
|
||||
left=ast.Field(
|
||||
chain=[events_alias, "properties", "$current_url"]
|
||||
if events_alias
|
||||
else ["properties", "$current_url"]
|
||||
chain=(
|
||||
[events_alias, "properties", "$current_url"]
|
||||
if events_alias
|
||||
else ["properties", "$current_url"]
|
||||
)
|
||||
),
|
||||
right=ast.Constant(value=step.url),
|
||||
)
|
||||
@@ -686,9 +686,11 @@ def action_to_expr(action: Action, events_alias: Optional[str] = None) -> ast.Ex
|
||||
expr = ast.CompareOperation(
|
||||
op=ast.CompareOperationOp.Regex,
|
||||
left=ast.Field(
|
||||
chain=[events_alias, "properties", "$current_url"]
|
||||
if events_alias
|
||||
else ["properties", "$current_url"]
|
||||
chain=(
|
||||
[events_alias, "properties", "$current_url"]
|
||||
if events_alias
|
||||
else ["properties", "$current_url"]
|
||||
)
|
||||
),
|
||||
right=ast.Constant(value=step.url),
|
||||
)
|
||||
@@ -696,9 +698,11 @@ def action_to_expr(action: Action, events_alias: Optional[str] = None) -> ast.Ex
|
||||
expr = ast.CompareOperation(
|
||||
op=ast.CompareOperationOp.Like,
|
||||
left=ast.Field(
|
||||
chain=[events_alias, "properties", "$current_url"]
|
||||
if events_alias
|
||||
else ["properties", "$current_url"]
|
||||
chain=(
|
||||
[events_alias, "properties", "$current_url"]
|
||||
if events_alias
|
||||
else ["properties", "$current_url"]
|
||||
)
|
||||
),
|
||||
right=ast.Constant(value=f"%{step.url}%"),
|
||||
)
|
||||
|
||||
2954
posthog/schema.py
@@ -35,7 +35,6 @@ from posthog.schema import (
|
||||
LifecycleQuery,
|
||||
StickinessActorsQuery,
|
||||
ErrorTrackingIssueFilter,
|
||||
ErrorTrackingIssuePropertyFilter,
|
||||
)
|
||||
|
||||
FilterType: TypeAlias = Union[Filter, PathFilter, RetentionFilter, StickinessFilter]
|
||||
@@ -70,7 +69,6 @@ AnyPropertyFilter: TypeAlias = Union[
|
||||
DataWarehousePropertyFilter,
|
||||
DataWarehousePersonPropertyFilter,
|
||||
ErrorTrackingIssueFilter,
|
||||
ErrorTrackingIssuePropertyFilter,
|
||||
]
|
||||
|
||||
EntityNode: TypeAlias = Union[EventsNode, ActionsNode, DataWarehouseNode]
|
||||
|
||||