Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ [RUM-4908] Add only Link to Facet List in Developer Extension #2830

Merged
merged 18 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(
cy-moi marked this conversation as resolved.
Show resolved Hide resolved
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