feat: rule editing (#31579)

This commit is contained in:
David Newell
2025-04-28 09:54:43 +01:00
committed by GitHub
parent 6077570073
commit 24dcb7e34f
45 changed files with 2060 additions and 1853 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@@ -227,7 +227,6 @@ export const definitionPopoverLogic = kea<definitionPopoverLogicType>([
TaxonomicFilterGroupType.NumericalEventProperties,
TaxonomicFilterGroupType.Metadata,
TaxonomicFilterGroupType.DataWarehousePersonProperties,
TaxonomicFilterGroupType.ErrorTrackingIssueProperties,
].includes(type) || type.startsWith(TaxonomicFilterGroupType.GroupsPrefix),
],
hasSentAs: [

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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[]> = {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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')],
}),
])

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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}%"),
)

File diff suppressed because it is too large Load Diff

View File

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