mirror of
https://github.com/BillyOutlast/posthog.git
synced 2026-02-04 03:01:23 +01:00
feat: add an operator allowlist to taxonomic filter (#36680)
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
@@ -5,7 +5,7 @@ import {
|
||||
OperatorValueSelectProps,
|
||||
} from 'lib/components/PropertyFilters/components/OperatorValueSelect'
|
||||
|
||||
import { PropertyDefinition, PropertyType } from '~/types'
|
||||
import { PropertyDefinition, PropertyOperator, PropertyType } from '~/types'
|
||||
|
||||
const meta: Meta<typeof OperatorValueSelect> = {
|
||||
title: 'Filters/PropertyFilters/OperatorValueSelect',
|
||||
@@ -20,21 +20,27 @@ const makePropertyDefinition = (name: string, propertyType: PropertyType | undef
|
||||
description: '',
|
||||
})
|
||||
|
||||
const props = (type: PropertyType | undefined, editable: boolean): OperatorValueSelectProps => ({
|
||||
const props = (overrides: {
|
||||
type?: PropertyType | undefined
|
||||
editable?: boolean
|
||||
startVisible?: boolean
|
||||
operatorAllowlist?: PropertyOperator[]
|
||||
}): OperatorValueSelectProps => ({
|
||||
type: undefined,
|
||||
propertyKey: 'the_property',
|
||||
onChange: () => {},
|
||||
propertyDefinitions: [makePropertyDefinition('the_property', type)],
|
||||
defaultOpen: true,
|
||||
editable,
|
||||
propertyDefinitions: [makePropertyDefinition('the_property', overrides.type)],
|
||||
editable: overrides.editable ?? false,
|
||||
startVisible: overrides.startVisible,
|
||||
operatorAllowlist: overrides.operatorAllowlist,
|
||||
})
|
||||
|
||||
export function OperatorValueWithStringProperty(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<h1>String Property</h1>
|
||||
<OperatorValueSelect {...props(PropertyType.String, true)} />
|
||||
<OperatorValueSelect {...props(PropertyType.String, false)} />
|
||||
<OperatorValueSelect {...props({ type: PropertyType.String, editable: true })} />
|
||||
<OperatorValueSelect {...props({ type: PropertyType.String, editable: false })} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -43,8 +49,8 @@ export function OperatorValueWithDateTimeProperty(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<h1>Date Time Property</h1>
|
||||
<OperatorValueSelect {...props(PropertyType.DateTime, true)} />
|
||||
<OperatorValueSelect {...props(PropertyType.DateTime, false)} />
|
||||
<OperatorValueSelect {...props({ type: PropertyType.DateTime, editable: true })} />
|
||||
<OperatorValueSelect {...props({ type: PropertyType.DateTime, editable: false })} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -53,8 +59,8 @@ export function OperatorValueWithNumericProperty(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<h1>Numeric Property</h1>
|
||||
<OperatorValueSelect {...props(PropertyType.Numeric, true)} />
|
||||
<OperatorValueSelect {...props(PropertyType.Numeric, false)} />
|
||||
<OperatorValueSelect {...props({ type: PropertyType.Numeric, editable: true })} />
|
||||
<OperatorValueSelect {...props({ type: PropertyType.Numeric, editable: false })} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -63,8 +69,8 @@ export function OperatorValueWithBooleanProperty(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<h1>Boolean Property</h1>
|
||||
<OperatorValueSelect {...props(PropertyType.Boolean, true)} />
|
||||
<OperatorValueSelect {...props(PropertyType.Boolean, false)} />
|
||||
<OperatorValueSelect {...props({ type: PropertyType.Boolean, editable: true })} />
|
||||
<OperatorValueSelect {...props({ type: PropertyType.Boolean, editable: false })} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -73,8 +79,8 @@ export function OperatorValueWithSelectorProperty(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<h1>CSS Selector Property</h1>
|
||||
<OperatorValueSelect {...props(PropertyType.Selector, true)} />
|
||||
<OperatorValueSelect {...props(PropertyType.Selector, false)} />
|
||||
<OperatorValueSelect {...props({ type: PropertyType.Selector, editable: true })} />
|
||||
<OperatorValueSelect {...props({ type: PropertyType.Selector, editable: false })} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -83,8 +89,36 @@ export function OperatorValueWithUnknownProperty(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<h1>Property without specific type</h1>
|
||||
<OperatorValueSelect {...props(undefined, true)} />
|
||||
<OperatorValueSelect {...props(undefined, false)} />
|
||||
<OperatorValueSelect {...props({ editable: true })} />
|
||||
<OperatorValueSelect {...props({ editable: false })} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function OperatorValueMenuOpen(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<h1>Showing the options</h1>
|
||||
<OperatorValueSelect {...props({ editable: true, startVisible: true })} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function OperatorValueMenuWithAllowlist(): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
<h1>Limiting the options to just three</h1>
|
||||
<OperatorValueSelect
|
||||
{...props({
|
||||
startVisible: true,
|
||||
editable: true,
|
||||
operatorAllowlist: [
|
||||
PropertyOperator.IContains,
|
||||
PropertyOperator.Exact,
|
||||
PropertyOperator.NotIContains,
|
||||
],
|
||||
})}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
import { LemonSelect, LemonSelectProps } from '@posthog/lemon-ui'
|
||||
import { LemonDropdownProps, LemonSelect, LemonSelectProps } from '@posthog/lemon-ui'
|
||||
|
||||
import { allOperatorsToHumanName } from 'lib/components/DefinitionPopover/utils'
|
||||
import { dayjs } from 'lib/dayjs'
|
||||
@@ -38,17 +38,23 @@ export interface OperatorValueSelectProps {
|
||||
operatorSelectProps?: Partial<Omit<LemonSelectProps<any>, 'onChange'>>
|
||||
eventNames?: string[]
|
||||
propertyDefinitions: PropertyDefinition[]
|
||||
defaultOpen?: boolean
|
||||
addRelativeDateTimeOptions?: boolean
|
||||
groupTypeIndex?: GroupTypeIndex
|
||||
size?: 'xsmall' | 'small' | 'medium'
|
||||
startVisible?: LemonDropdownProps['startVisible']
|
||||
/**
|
||||
* in some contexts you want to externally limit the available operators
|
||||
* this won't add an operator if it isn't valid
|
||||
* i.e. it limits the options shown from the options that would have been shown
|
||||
* **/
|
||||
operatorAllowlist?: Array<PropertyOperator>
|
||||
}
|
||||
|
||||
interface OperatorSelectProps extends Omit<LemonSelectProps<any>, 'options'> {
|
||||
operator: PropertyOperator
|
||||
operators: Array<PropertyOperator>
|
||||
onChange: (operator: PropertyOperator) => void
|
||||
defaultOpen?: boolean
|
||||
startVisible?: LemonDropdownProps['startVisible']
|
||||
}
|
||||
|
||||
function getValidationError(operator: PropertyOperator, value: any, property?: string): string | null {
|
||||
@@ -81,11 +87,12 @@ export function OperatorValueSelect({
|
||||
operatorSelectProps,
|
||||
propertyDefinitions = [],
|
||||
eventNames = [],
|
||||
defaultOpen,
|
||||
addRelativeDateTimeOptions,
|
||||
groupTypeIndex = undefined,
|
||||
size,
|
||||
editable,
|
||||
startVisible,
|
||||
operatorAllowlist,
|
||||
}: OperatorValueSelectProps): JSX.Element {
|
||||
const lookupKey = type === PropertyFilterType.DataWarehousePersonProperty ? 'id' : 'name'
|
||||
const propertyDefinition = propertyDefinitions.find((pd) => pd[lookupKey] === propertyKey)
|
||||
@@ -132,7 +139,9 @@ export function OperatorValueSelect({
|
||||
|
||||
const operatorMapping: Record<string, string> = chooseOperatorMap(propertyType)
|
||||
|
||||
const operators = Object.keys(operatorMapping) as Array<PropertyOperator>
|
||||
const operators = (Object.keys(operatorMapping) as Array<PropertyOperator>).filter((op) => {
|
||||
return !operatorAllowlist || operatorAllowlist.includes(op)
|
||||
})
|
||||
setOperators(operators)
|
||||
if ((currentOperator !== operator && operators.includes(startingOperator)) || !propertyDefinition) {
|
||||
setCurrentOperator(startingOperator)
|
||||
@@ -147,7 +156,7 @@ export function OperatorValueSelect({
|
||||
}
|
||||
setCurrentOperator(defaultProperty)
|
||||
}
|
||||
}, [propertyDefinition, propertyKey, operator]) // oxlint-disable-line react-hooks/exhaustive-deps
|
||||
}, [propertyDefinition, propertyKey, operator, operatorAllowlist]) // oxlint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -184,7 +193,7 @@ export function OperatorValueSelect({
|
||||
}}
|
||||
{...operatorSelectProps}
|
||||
size={size}
|
||||
defaultOpen={defaultOpen}
|
||||
startVisible={startVisible}
|
||||
/>
|
||||
) : (
|
||||
<span>{allOperatorsToHumanName(currentOperator)} </span>
|
||||
@@ -232,7 +241,14 @@ export function OperatorValueSelect({
|
||||
)
|
||||
}
|
||||
|
||||
export function OperatorSelect({ operator, operators, onChange, className, size }: OperatorSelectProps): JSX.Element {
|
||||
export function OperatorSelect({
|
||||
operator,
|
||||
operators,
|
||||
onChange,
|
||||
className,
|
||||
size,
|
||||
startVisible,
|
||||
}: OperatorSelectProps): JSX.Element {
|
||||
const operatorOptions = operators.map((op) => ({
|
||||
label: <span className="operator-value-option">{allOperatorsMapping[op || PropertyOperator.Exact]}</span>,
|
||||
value: op || PropertyOperator.Exact,
|
||||
@@ -252,6 +268,7 @@ export function OperatorSelect({ operator, operators, onChange, className, size
|
||||
menu={{
|
||||
closeParentPopoverOnClickInside: false,
|
||||
}}
|
||||
startVisible={startVisible}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ export function TaxonomicPropertyFilter({
|
||||
hideBehavioralCohorts,
|
||||
addFilterDocLink,
|
||||
editable = true,
|
||||
operatorAllowlist,
|
||||
}: PropertyFilterInternalProps): JSX.Element {
|
||||
const pageKey = useMemo(() => pageKeyInput || `filter-${uniqueMemoizedIndex++}`, [pageKeyInput])
|
||||
const groupTypes = taxonomicGroupTypes || DEFAULT_TAXONOMIC_GROUP_TYPES
|
||||
@@ -184,6 +185,7 @@ export function TaxonomicPropertyFilter({
|
||||
? (filter?.group_type_index as GroupTypeIndex)
|
||||
: undefined
|
||||
}
|
||||
operatorAllowlist={operatorAllowlist}
|
||||
/>
|
||||
)
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { OperatorValueSelectProps } from 'lib/components/PropertyFilters/components/OperatorValueSelect'
|
||||
import {
|
||||
ExcludedProperties,
|
||||
TaxonomicFilterGroup,
|
||||
@@ -42,8 +43,10 @@ export interface PropertyFilterInternalProps {
|
||||
filters: AnyPropertyFilter[]
|
||||
setFilter: (index: number, property: AnyPropertyFilter) => void
|
||||
editable?: boolean
|
||||
operatorAllowlist?: OperatorValueSelectProps['operatorAllowlist']
|
||||
taxonomicGroupTypes?: TaxonomicFilterGroupType[]
|
||||
taxonomicFilterOptionsFromProp?: TaxonomicFilterProps['optionsFromProp']
|
||||
propertyAllowList?: { [key in TaxonomicFilterGroupType]?: string[] }
|
||||
eventNames?: string[]
|
||||
schemaColumns?: DatabaseSchemaField[]
|
||||
propertyGroupType?: FilterLogicalOperator | null
|
||||
@@ -52,7 +55,6 @@ export interface PropertyFilterInternalProps {
|
||||
size?: 'xsmall' | 'small' | 'medium'
|
||||
hasRowOperator?: boolean
|
||||
metadataSource?: AnyDataNode
|
||||
propertyAllowList?: { [key in TaxonomicFilterGroupType]?: string[] }
|
||||
excludedProperties?: ExcludedProperties
|
||||
allowRelativeDateOptions?: boolean
|
||||
exactMatchFeatureFlagCohortOperators?: boolean
|
||||
|
||||
@@ -4,6 +4,8 @@ import { useState } from 'react'
|
||||
import { IconPlusSmall } from '@posthog/icons'
|
||||
import { LemonButton, LemonButtonProps, LemonDropdown, Popover } from '@posthog/lemon-ui'
|
||||
|
||||
import { OperatorValueSelectProps } from 'lib/components/PropertyFilters/components/OperatorValueSelect'
|
||||
|
||||
import { AnyDataNode } from '~/queries/schema/schema-general'
|
||||
import { UniversalFilterValue, UniversalFiltersGroup } from '~/types'
|
||||
|
||||
@@ -79,6 +81,7 @@ const Value = ({
|
||||
initiallyOpen = false,
|
||||
metadataSource,
|
||||
className,
|
||||
operatorAllowlist,
|
||||
}: {
|
||||
index: number
|
||||
filter: UniversalFilterValue
|
||||
@@ -87,6 +90,7 @@ const Value = ({
|
||||
initiallyOpen?: boolean
|
||||
metadataSource?: AnyDataNode
|
||||
className?: string
|
||||
operatorAllowlist?: OperatorValueSelectProps['operatorAllowlist']
|
||||
}): JSX.Element => {
|
||||
const { rootKey, taxonomicPropertyFilterGroupTypes } = useValues(universalFiltersLogic)
|
||||
|
||||
@@ -124,6 +128,7 @@ const Value = ({
|
||||
setFilter={(_, property) => onChange(property)}
|
||||
disablePopover={false}
|
||||
taxonomicGroupTypes={taxonomicPropertyFilterGroupTypes}
|
||||
operatorAllowlist={operatorAllowlist}
|
||||
/>
|
||||
) : null
|
||||
}
|
||||
|
||||
@@ -4,6 +4,12 @@ import { Popover, PopoverOverlayContext, PopoverProps } from '../Popover'
|
||||
|
||||
export interface LemonDropdownProps extends Omit<PopoverProps, 'children' | 'visible'> {
|
||||
visible?: boolean
|
||||
/**
|
||||
* Setting `visible` shifts the component to controlled mode.
|
||||
* This lets you choose whether to start open (Defaults to false).
|
||||
* Without having to take control of the visibility state.
|
||||
* */
|
||||
startVisible?: boolean
|
||||
onVisibilityChange?: (visible: boolean) => void
|
||||
/**
|
||||
* Whether the dropdown should be closed on click inside.
|
||||
@@ -34,6 +40,7 @@ export const LemonDropdown: React.FunctionComponent<LemonDropdownProps & React.R
|
||||
closeOnClickInside = true,
|
||||
trigger = 'click',
|
||||
children,
|
||||
startVisible,
|
||||
...popoverProps
|
||||
},
|
||||
ref
|
||||
@@ -41,7 +48,7 @@ export const LemonDropdown: React.FunctionComponent<LemonDropdownProps & React.R
|
||||
const isControlled = visible !== undefined
|
||||
|
||||
const [, parentPopoverLevel] = useContext(PopoverOverlayContext)
|
||||
const [localVisible, setLocalVisible] = useState(visible ?? false)
|
||||
const [localVisible, setLocalVisible] = useState(visible ?? startVisible ?? false)
|
||||
|
||||
const floatingRef = useRef<HTMLDivElement>(null)
|
||||
const referenceRef = useRef<HTMLSpanElement>(null)
|
||||
|
||||
@@ -95,6 +95,7 @@ export interface LemonMenuProps
|
||||
| 'className'
|
||||
| 'onClickOutside'
|
||||
| 'middleware'
|
||||
| 'startVisible'
|
||||
>,
|
||||
LemonMenuOverlayProps {
|
||||
/** Must support `ref` and `onKeyDown` for keyboard navigation. */
|
||||
|
||||
@@ -3,6 +3,8 @@ import React, { useMemo } from 'react'
|
||||
|
||||
import { IconX } from '@posthog/icons'
|
||||
|
||||
import { LemonDropdownProps } from 'lib/lemon-ui/LemonDropdown'
|
||||
|
||||
import { LemonButton, LemonButtonProps } from '../LemonButton'
|
||||
import {
|
||||
LemonMenu,
|
||||
@@ -77,7 +79,8 @@ export interface LemonSelectPropsBase<T>
|
||||
placeholder?: string
|
||||
size?: LemonButtonProps['size']
|
||||
menu?: Pick<LemonMenuProps, 'className' | 'closeParentPopoverOnClickInside'>
|
||||
visible?: boolean
|
||||
visible?: LemonDropdownProps['visible']
|
||||
startVisible?: LemonDropdownProps['startVisible']
|
||||
}
|
||||
|
||||
export interface LemonSelectPropsClearable<T> extends LemonSelectPropsBase<T> {
|
||||
@@ -115,6 +118,7 @@ export function LemonSelect<T extends string | number | boolean | null>({
|
||||
menu,
|
||||
renderButtonContent,
|
||||
visible,
|
||||
startVisible,
|
||||
...buttonProps
|
||||
}: LemonSelectProps<T>): JSX.Element {
|
||||
const [items, allLeafOptions] = useMemo(
|
||||
@@ -144,6 +148,7 @@ export function LemonSelect<T extends string | number | boolean | null>({
|
||||
.findIndex((i) => (i as LemonMenuItem).active)}
|
||||
closeParentPopoverOnClickInside={menu?.closeParentPopoverOnClickInside}
|
||||
visible={visible}
|
||||
startVisible={startVisible}
|
||||
>
|
||||
<LemonButton
|
||||
className={clsx(className, 'LemonSelect')}
|
||||
|
||||
Reference in New Issue
Block a user