Skip to content

Commit

Permalink
✨ [RUM-4908] Add only Link to Facet List in Developer Extension (#2830
Browse files Browse the repository at this point in the history
)

* Re-implement with standalone compute select status

* Clean up code

* Fix switching from exclude to include

* Add parents check for include mode

* Simplify toggle facet value

* Simplify compute status and fix tests

* Simplify facet select mode switching

* Add compute select test coverage

* Make the behavior consistent with web-ui

* Enhance code and add tests

* Improve event filters test

* Fix filter facets in include mode

* Reset when unchecking children in Only mode

* Polish UI and code

* Polish and add testsfor event filter

* Restore changes to computeFacetState
  • Loading branch information
cy-moi authored Feb 5, 2025
1 parent 537b9ee commit c907a82
Show file tree
Hide file tree
Showing 8 changed files with 458 additions and 54 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import type { RumActionEvent, RumResourceEvent } from '@datadog/browser-rum'
import { FacetRegistry } from '../../../hooks/useEvents'
import type { FacetValuesFilter } from '../../../hooks/useEvents'
import { FACET_ROOT } from '../../../facets.constants'
import type { Facet } from '../../../facets.constants'
import { computeSelectionState } from './computeFacetState'

const rumResourceXHREvent = {
type: 'resource',
resource: {
type: 'xhr',
url: 'http://example.com',
},
} as RumResourceEvent

const rumResourceBeaconEvent = {
type: 'resource',
resource: {
type: 'beacon',
url: 'http://example.com',
},
} as RumResourceEvent

const rumCustomActionEvent = {
type: 'action',
action: {
type: 'custom',
},
} as RumActionEvent

// test that computeSelectionState returns the correct state
describe('computeSelectionState', () => {
describe('include mode', () => {
it('returns "unselected" when the filter is empty', () => {
const facetValuesFilter: FacetValuesFilter = {
type: 'include',
facetValues: {},
}
const facet = {
path: 'resource.type',
label: 'Resource Type',
}

const facetRegistry = new FacetRegistry()
facetRegistry.addEvent(rumResourceXHREvent)
const facetValue = 'xhr'
expect(
computeSelectionState(facetValuesFilter, facetRegistry, facet, facetValue, ['rum', 'resource', 'xhr'])
).toBe('unselected')
})
it('returns "selected" when the facet is in the filter', () => {
const facetValuesFilter: FacetValuesFilter = {
type: 'include',
facetValues: {
'resource.type': ['xhr'],
},
}
const facet = {
path: 'resource.type',
label: 'Resource Type',
}

const facetRegistry = new FacetRegistry()
facetRegistry.addEvent(rumResourceXHREvent)
const facetValue = 'xhr'
expect(
computeSelectionState(facetValuesFilter, facetRegistry, facet, facetValue, ['rum', 'resource', 'xhr'])
).toBe('selected')
})

it('returns "partial-selected" when some children are in the filter', () => {
const facetValuesFilter: FacetValuesFilter = {
type: 'include',
facetValues: {
'resource.type': ['xhr'],
},
}
const facet = FACET_ROOT.values!.rum?.facets![0] as Facet
const facetValue = 'resource'
const facetRegistry = new FacetRegistry()
facetRegistry.addEvent(rumResourceXHREvent)
facetRegistry.addEvent(rumResourceBeaconEvent)
expect(computeSelectionState(facetValuesFilter, facetRegistry, facet, facetValue, ['rum', 'resource'])).toBe(
'partial-selected'
)
})

it('returns "selected" when all children are in the filter', () => {
const facetValuesFilter: FacetValuesFilter = {
type: 'include',
facetValues: {
'resource.type': ['xhr', 'beacon'],
},
}
const facet = FACET_ROOT.values!.rum?.facets![0] as Facet
const facetValue = 'resource'
const facetRegistry = new FacetRegistry()
facetRegistry.addEvent(rumResourceXHREvent)
facetRegistry.addEvent(rumResourceBeaconEvent)
expect(computeSelectionState(facetValuesFilter, facetRegistry, facet, facetValue, ['rum', 'resource'])).toBe(
'selected'
)
})

it('returns "unselected" when the facet or children are not in the filter', () => {
const facetValuesFilter: FacetValuesFilter = {
type: 'include',
facetValues: {
'resource.type': ['xhr'],
},
}
const facet = {
path: 'action.type',
label: 'Action Type',
}

const facetValue = 'action'
const facetRegistry = new FacetRegistry()
facetRegistry.addEvent(rumResourceXHREvent)
facetRegistry.addEvent(rumCustomActionEvent)
expect(computeSelectionState(facetValuesFilter, facetRegistry, facet, facetValue, ['rum', 'action'])).toBe(
'unselected'
)
})
})

describe('exclude mode', () => {
it('returns "selected" when the filter is empty', () => {
const facetValuesFilter: FacetValuesFilter = {
type: 'exclude',
facetValues: {},
}
const facet = {
path: 'resource.type',
label: 'Resource Type',
}
const facetRegistry = new FacetRegistry()
facetRegistry.addEvent(rumResourceXHREvent)
const facetValue = 'xhr'
expect(
computeSelectionState(facetValuesFilter, facetRegistry, facet, facetValue, ['rum', 'resource', 'xhr'])
).toBe('selected')
})

it('returns "unselected" when the facet is in the filter', () => {
const facetValuesFilter: FacetValuesFilter = {
type: 'exclude',
facetValues: {
'resource.type': ['xhr'],
},
}
const facet = {
path: 'resource.type',
label: 'Resource Type',
}
const facetRegistry = new FacetRegistry()
facetRegistry.addEvent(rumResourceXHREvent)
const facetValue = 'xhr'
expect(
computeSelectionState(facetValuesFilter, facetRegistry, facet, facetValue, ['rum', 'resource', 'xhr'])
).toBe('unselected')
})
it('returns "partial-selected" when some children are in the filter', () => {
const facetValuesFilter: FacetValuesFilter = {
type: 'exclude',
facetValues: {
'resource.type': ['xhr'],
},
}
const facet = FACET_ROOT.values!.rum?.facets![0] as Facet

const facetValue = 'resource'
const facetRegistry = new FacetRegistry()
facetRegistry.addEvent(rumResourceXHREvent)
facetRegistry.addEvent(rumResourceBeaconEvent)
expect(computeSelectionState(facetValuesFilter, facetRegistry, facet, facetValue, ['rum', 'resource'])).toBe(
'partial-selected'
)
})

it('returns "unelected" when all children are in the filter', () => {
const facetValuesFilter: FacetValuesFilter = {
type: 'exclude',
facetValues: {
'resource.type': ['xhr', 'beacon'],
},
}
const facet = FACET_ROOT.values!.rum?.facets![0] as Facet
const facetValue = 'resource'
const facetRegistry = new FacetRegistry()
facetRegistry.addEvent(rumResourceXHREvent)
facetRegistry.addEvent(rumResourceBeaconEvent)
expect(computeSelectionState(facetValuesFilter, facetRegistry, facet, facetValue, ['rum', 'resource'])).toBe(
'unselected'
)
})

it('returns "selected" when the facet or children are not in the filter', () => {
const facetValuesFilter: FacetValuesFilter = {
type: 'exclude',
facetValues: {
'resource.type': ['xhr'],
},
}
const facet = {
path: 'action.type',
label: 'Action Type',
}

const facetValue = 'action'
const facetRegistry = new FacetRegistry()
facetRegistry.addEvent(rumResourceXHREvent)
facetRegistry.addEvent(rumCustomActionEvent)
expect(computeSelectionState(facetValuesFilter, facetRegistry, facet, facetValue, ['rum', 'action'])).toBe(
'selected'
)
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type { FacetRegistry, FacetValuesFilter } from '../../../hooks/useEvents'
import type { Facet, FacetValue } from '../../../facets.constants'
type SelectionState = 'selected' | 'unselected' | 'partial-selected'

function isAllChildrenFiltered(children: string[], filteredFacetValues: string[]) {
return children.every((child: FacetValue) => filteredFacetValues.includes(child))
}

function isAnyChildrenFiltered(children: string[], filteredFacetValues: string[]) {
return children.some((child: FacetValue) => filteredFacetValues.includes(child))
}

// limitation: only populate direct parents
export function computeSelectionState(
facetValuesFilter: FacetValuesFilter,
facetRegistry: FacetRegistry,
facet: Facet,
facetValue: FacetValue,
parentList: string[]
): SelectionState {
const childrenFacets = facet.values?.[facetValue]?.facets

// we cannot know how many children in total there are, so we need to have facetRegistry
const children =
childrenFacets && childrenFacets.flatMap((child: Facet) => facetRegistry.getFacetChildrenValues(child.path))
const filteredFacetValues = Object.values(facetValuesFilter.facetValues).flat()
const isFiltering = !!Object.keys(facetValuesFilter.facetValues)

if (facetValuesFilter.type === 'include') {
if (!isFiltering) {
return 'unselected'
}

for (const parent of parentList) {
if (filteredFacetValues.includes(parent)) {
return 'selected'
}
}

// if all children are in the filter, then it should be selected'
if (children && isAllChildrenFiltered(children, filteredFacetValues)) {
return 'selected'
}
// if any of the direct children of the facet is in the filter, then it should be partial-selected
if (children && isAnyChildrenFiltered(children, filteredFacetValues)) {
return 'partial-selected'
}
} else if (facetValuesFilter.type === 'exclude') {
if (!isFiltering) {
return 'selected'
}
// if facet.value is in facetValueFilter, then it should be unselected
if (filteredFacetValues.includes(facetValue)) {
return 'unselected'
}
// if all children are in the filter, then it should be unselected
if (children && isAllChildrenFiltered(children, filteredFacetValues)) {
return 'unselected'
}
// if any of the children of the facet is in the filter, then it should be partial-selected
if (children && isAnyChildrenFiltered(children, filteredFacetValues)) {
return 'partial-selected'
}
return 'selected'
}

return 'unselected'
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ export function EventsTabSide({
{facetRegistry && (
<FacetList
facetRegistry={facetRegistry}
excludedFacetValues={filters.excludedFacetValues}
facetValuesFilter={filters.facetValuesFilter}
onExcludedFacetValuesChange={(newExcludedFacetValues) =>
onFiltersChange({ ...filters, excludedFacetValues: newExcludedFacetValues })
onFiltersChange({ ...filters, facetValuesFilter: newExcludedFacetValues })
}
/>
)}
Expand Down
Loading

0 comments on commit c907a82

Please sign in to comment.