diff --git a/static/app/views/explore/components/traceItemSearchQueryBuilder.spec.tsx b/static/app/views/explore/components/traceItemSearchQueryBuilder.spec.tsx index 42ac973ced99ff..e2e6024bff7b59 100644 --- a/static/app/views/explore/components/traceItemSearchQueryBuilder.spec.tsx +++ b/static/app/views/explore/components/traceItemSearchQueryBuilder.spec.tsx @@ -280,6 +280,30 @@ describe('useTraceItemSearchQueryBuilderProps', () => { }); }); + it('uses a custom placeholder when provided', () => { + const {result} = renderHookWithProviders(useTraceItemSearchQueryBuilderProps, { + initialProps: { + ...defaultInitialProps, + placeholder: 'Custom placeholder text', + }, + organization, + }); + + expect(result.current.placeholder).toBe('Custom placeholder text'); + }); + + it('falls back to the default placeholder for logs when no placeholder is provided', () => { + const {result} = renderHookWithProviders(useTraceItemSearchQueryBuilderProps, { + initialProps: { + ...defaultInitialProps, + itemType: TraceItemDataset.LOGS, + }, + organization, + }); + + expect(result.current.placeholder).toBe('Search for logs, users, tags, and more'); + }); + it('calls validateQuery when a new filter key is added', async () => { const validateMock = MockApiClient.addMockResponse({ url: '/organizations/org-slug/trace-items/attributes/validate/', diff --git a/static/app/views/explore/components/traceItemSearchQueryBuilder.tsx b/static/app/views/explore/components/traceItemSearchQueryBuilder.tsx index 8cddfe407f7d35..97acf2447a45a3 100644 --- a/static/app/views/explore/components/traceItemSearchQueryBuilder.tsx +++ b/static/app/views/explore/components/traceItemSearchQueryBuilder.tsx @@ -111,8 +111,9 @@ export function useTraceItemSearchQueryBuilderProps({ attributeQuery, hiddenAttributeKeys, allowedAttributeKeys, + placeholder, }: TraceItemSearchQueryBuilderProps) { - const placeholderText = itemTypeToDefaultPlaceholder(itemType); + const placeholderText = placeholder ?? itemTypeToDefaultPlaceholder(itemType); const {selection} = usePageFilters(); const effectiveProjects = projects ?? selection.projects; @@ -259,6 +260,7 @@ export function TraceItemSearchQueryBuilder({ attributeQuery, hiddenAttributeKeys, allowedAttributeKeys, + placeholder, }: TraceItemSearchQueryBuilderProps) { const searchQueryBuilderProps = useTraceItemSearchQueryBuilderProps({ itemType, @@ -289,6 +291,7 @@ export function TraceItemSearchQueryBuilder({ attributeQuery, hiddenAttributeKeys, allowedAttributeKeys, + placeholder, }); return ( diff --git a/static/app/views/explore/logs/useLogsSearchQueryBuilderProps.tsx b/static/app/views/explore/logs/useLogsSearchQueryBuilderProps.tsx index cc74fe76ca8a90..98fc324a10d62a 100644 --- a/static/app/views/explore/logs/useLogsSearchQueryBuilderProps.tsx +++ b/static/app/views/explore/logs/useLogsSearchQueryBuilderProps.tsx @@ -18,6 +18,7 @@ import {TraceItemDataset} from 'sentry/views/explore/types'; import {findSuggestedColumns} from 'sentry/views/explore/utils'; export function useLogsSearchQueryBuilderProps({ + attributeQuery, booleanAttributes, booleanSecondaryAliases, numberAttributes, @@ -31,6 +32,7 @@ export function useLogsSearchQueryBuilderProps({ numberSecondaryAliases: TagCollection; stringAttributes: TagCollection; stringSecondaryAliases: TagCollection; + attributeQuery?: string; }) { const logsSearch = useQueryParamsSearch(); const oldLogsSearch = usePrevious(logsSearch); @@ -83,8 +85,10 @@ export function useLogsSearchQueryBuilderProps({ replaceRawSearchKeys: ['message'], matchKeySuggestions: [{key: 'trace', valuePattern: /^[0-9a-fA-F]{32}$/}], hiddenAttributeKeys: HiddenLogSearchFields, + attributeQuery, }), [ + attributeQuery, booleanAttributes, booleanSecondaryAliases, caseInsensitive, diff --git a/static/app/views/explore/replays/detail/noRowRenderer.spec.tsx b/static/app/views/explore/replays/detail/noRowRenderer.spec.tsx new file mode 100644 index 00000000000000..6f59aa31908c36 --- /dev/null +++ b/static/app/views/explore/replays/detail/noRowRenderer.spec.tsx @@ -0,0 +1,69 @@ +import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary'; + +import {NoRowRenderer} from 'sentry/views/explore/replays/detail/noRowRenderer'; + +const children = 'No logs yet'; + +describe('NoRowRenderer', () => { + it('renders children when unfilteredItems is empty', () => { + render( + + {children} + + ); + + expect(screen.getByText('No logs yet')).toBeInTheDocument(); + expect(screen.queryByText('No results found')).not.toBeInTheDocument(); + }); + + it('renders no results state when unfilteredItems has items', () => { + render( + + {children} + + ); + + expect(screen.getByText('No results found')).toBeInTheDocument(); + expect(screen.getByRole('button', {name: 'Clear filters'})).toBeInTheDocument(); + expect(screen.queryByText('No logs yet')).not.toBeInTheDocument(); + }); + + it('calls clearSearchTerm when Clear filters is clicked', async () => { + const clearSearchTerm = jest.fn(); + render( + + {children} + + ); + + await userEvent.click(screen.getByRole('button', {name: 'Clear filters'})); + + expect(clearSearchTerm).toHaveBeenCalledTimes(1); + }); + + it('uses hasUnfilteredItems prop over unfilteredItems array length', () => { + render( + + {children} + + ); + + expect(screen.getByText('No results found')).toBeInTheDocument(); + expect(screen.queryByText('No logs yet')).not.toBeInTheDocument(); + }); + + it('renders children when hasUnfilteredItems is false even with items in array', () => { + render( + + {children} + + ); + + expect(screen.getByText('No logs yet')).toBeInTheDocument(); + expect(screen.queryByText('No results found')).not.toBeInTheDocument(); + }); +}); diff --git a/static/app/views/explore/replays/detail/noRowRenderer.tsx b/static/app/views/explore/replays/detail/noRowRenderer.tsx index 826426ff14b2c2..e1fdb367233836 100644 --- a/static/app/views/explore/replays/detail/noRowRenderer.tsx +++ b/static/app/views/explore/replays/detail/noRowRenderer.tsx @@ -10,19 +10,27 @@ type Props = { children: ReactNode; clearSearchTerm: () => void; unfilteredItems: unknown[]; + hasUnfilteredItems?: boolean; }; -export function NoRowRenderer({children, unfilteredItems, clearSearchTerm}: Props) { - return unfilteredItems.length === 0 ? ( - -

{children}

-
- ) : ( +export function NoRowRenderer({ + children, + unfilteredItems, + hasUnfilteredItems, + clearSearchTerm, +}: Props) { + const itemsExist = hasUnfilteredItems ?? unfilteredItems.length > 0; + + return itemsExist ? (

{t('No results found')}

+ ) : ( + +

{children}

+
); } diff --git a/static/app/views/explore/replays/detail/ourlogs/index.tsx b/static/app/views/explore/replays/detail/ourlogs/index.tsx index 4c465a43a26af5..533c0e35831c37 100644 --- a/static/app/views/explore/replays/detail/ourlogs/index.tsx +++ b/static/app/views/explore/replays/detail/ourlogs/index.tsx @@ -1,4 +1,4 @@ -import {useCallback, useMemo} from 'react'; +import {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import styled from '@emotion/styled'; import {Placeholder} from 'sentry/components/placeholder'; @@ -9,6 +9,7 @@ import {defined} from 'sentry/utils'; import {LogsAnalyticsPageSource} from 'sentry/utils/analytics/logsAnalyticsEvent'; import {useReplayReader} from 'sentry/utils/replays/playback/providers/replayReaderProvider'; import {useCurrentHoverTime} from 'sentry/utils/replays/playback/providers/useCurrentHoverTime'; +import {MutableSearch} from 'sentry/utils/tokenizeSearch'; import {defaultLogFields} from 'sentry/views/explore/contexts/logs/fields'; import { LogsPageDataProvider, @@ -23,11 +24,15 @@ import { LogsInfiniteTable, } from 'sentry/views/explore/logs/tables/logsInfiniteTable'; import {rearrangedLogsReplayFields} from 'sentry/views/explore/logs/tables/logsTableUtils'; +import {useLogsSearchQueryBuilderProps} from 'sentry/views/explore/logs/useLogsSearchQueryBuilderProps'; +import { + useQueryParamsSearch, + useSetQueryParamsSearch, +} from 'sentry/views/explore/queryParams/context'; import {FluidHeight} from 'sentry/views/explore/replays/detail/layout/fluidHeight'; import {NoRowRenderer} from 'sentry/views/explore/replays/detail/noRowRenderer'; import {OurLogFilters} from 'sentry/views/explore/replays/detail/ourlogs/ourlogFilters'; import {ourlogsAsFrames} from 'sentry/views/explore/replays/detail/ourlogs/ourlogsAsFrames'; -import {useOurLogFilters} from 'sentry/views/explore/replays/detail/ourlogs/useOurLogFilters'; export function OurLogs() { const replay = useReplayReader(); @@ -71,9 +76,15 @@ interface OurLogsContentProps { } function OurLogsContent({replayId, startTimestampMs}: OurLogsContentProps) { - const {attributes: stringAttributes} = useLogItemAttributes({}, 'string'); - const {attributes: numberAttributes} = useLogItemAttributes({}, 'number'); - const {attributes: booleanAttributes} = useLogItemAttributes({}, 'boolean'); + const replayAttributeFilter = MutableSearch.fromQueryObject({ + [`sentry._internal.cooccuring.replay_id.${replayId}`]: ['true'], + }).formatString(); + const {attributes: stringAttributes, secondaryAliases: stringSecondaryAliases} = + useLogItemAttributes({query: replayAttributeFilter}, 'string'); + const {attributes: numberAttributes, secondaryAliases: numberSecondaryAliases} = + useLogItemAttributes({query: replayAttributeFilter}, 'number'); + const {attributes: booleanAttributes, secondaryAliases: booleanSecondaryAliases} = + useLogItemAttributes({query: replayAttributeFilter}, 'boolean'); const {currentTime, setCurrentTime} = useReplayContext(); const [currentHoverTime] = useCurrentHoverTime(); @@ -82,9 +93,37 @@ function OurLogsContent({replayId, startTimestampMs}: OurLogsContentProps) { const {infiniteLogsQueryResult} = useLogsPageData(); const {data: logItems, isPending} = infiniteLogsQueryResult; - const filterProps = useOurLogFilters({logItems}); - const {items: filteredLogItems, setSearchTerm} = filterProps; - const clearSearchTerm = () => setSearchTerm(''); + const logsSearch = useQueryParamsSearch(); + const setLogsSearch = useSetQueryParamsSearch(); + const filterText = logsSearch.freeText.join(' '); + const clearSearch = useCallback( + () => setLogsSearch(new MutableSearch('')), + [setLogsSearch] + ); + + const [hasAnyLogs, setHasAnyLogs] = useState(!!logItems?.length); + const previousReplayId = useRef(replayId); + useEffect(() => { + if (previousReplayId.current !== replayId) { + previousReplayId.current = replayId; + setHasAnyLogs(!!logItems?.length); + return; + } + if (logItems?.length) { + setHasAnyLogs(true); + } + }, [logItems, replayId]); + + const {tracesItemSearchQueryBuilderProps, searchQueryBuilderProviderProps} = + useLogsSearchQueryBuilderProps({ + attributeQuery: replayAttributeFilter, + stringAttributes, + numberAttributes, + booleanAttributes, + stringSecondaryAliases, + numberSecondaryAliases, + booleanSecondaryAliases, + }); const handleReplayTimeClick = useCallback( (offsetMs: string) => { @@ -120,7 +159,11 @@ function OurLogsContent({replayId, startTimestampMs}: OurLogsContentProps) { return ( - + {isPending ? ( @@ -133,12 +176,16 @@ function OurLogsContent({replayId, startTimestampMs}: OurLogsContentProps) { embedded embeddedOptions={embeddedOptions} localOnlyItemFilters={{ - filteredItems: filteredLogItems, - filterText: filterProps.searchTerm, + filteredItems: logItems, + filterText, }} embeddedStyling={{disableBodyPadding: true, showVerticalScrollbar: false}} emptyRenderer={() => ( - + {t('No logs recorded')} )} diff --git a/static/app/views/explore/replays/detail/ourlogs/openInLogsButton.spec.tsx b/static/app/views/explore/replays/detail/ourlogs/openInLogsButton.spec.tsx new file mode 100644 index 00000000000000..dea3f230b74a45 --- /dev/null +++ b/static/app/views/explore/replays/detail/ourlogs/openInLogsButton.spec.tsx @@ -0,0 +1,93 @@ +import {OrganizationFixture} from 'sentry-fixture/organization'; + +import {render, screen} from 'sentry-test/reactTestingLibrary'; + +import {PageFiltersStore} from 'sentry/components/pageFilters/store'; +import {LogsAnalyticsPageSource} from 'sentry/utils/analytics/logsAnalyticsEvent'; +import {LogsQueryParamsProvider} from 'sentry/views/explore/logs/logsQueryParamsProvider'; +import {OpenInLogsButton} from 'sentry/views/explore/replays/detail/ourlogs/openInLogsButton'; + +function Wrapper({children}: {children: React.ReactNode}) { + return ( + + {children} + + ); +} + +describe('OpenInLogsButton', () => { + beforeEach(() => { + PageFiltersStore.init(); + PageFiltersStore.onInitializeUrlState({ + projects: [1], + environments: [], + datetime: {period: '14d', start: null, end: null, utc: false}, + }); + }); + + it('renders nothing when the explore feature flag is disabled', () => { + const organization = OrganizationFixture({features: []}); + + const {container} = render( + + + , + {organization} + ); + + expect(container).toBeEmptyDOMElement(); + }); + + it('renders the button when the explore feature flag is enabled', () => { + const organization = OrganizationFixture({ + features: ['visibility-explore-view'], + }); + + render( + + + , + {organization} + ); + + expect(screen.getByRole('button', {name: 'Open in Logs'})).toBeInTheDocument(); + }); + + it('appends replay_id to the URL when replayId is provided', () => { + const organization = OrganizationFixture({ + features: ['visibility-explore-view'], + }); + + render( + + + , + {organization} + ); + + const link = screen.getByRole('button', {name: 'Open in Logs'}); + expect(link).toHaveAttribute('href', expect.stringContaining('replay_id%3Adeadbeef')); + }); + + it('includes the existing search query before replay_id in the URL', () => { + const organization = OrganizationFixture({ + features: ['visibility-explore-view'], + }); + + render( + + + , + {organization} + ); + + const link = screen.getByRole('button', {name: 'Open in Logs'}); + expect(link).toHaveAttribute( + 'href', + expect.stringMatching(/logsQuery=.*replay_id%3Adeadbeef/) + ); + }); +}); diff --git a/static/app/views/explore/replays/detail/ourlogs/openInLogsButton.tsx b/static/app/views/explore/replays/detail/ourlogs/openInLogsButton.tsx index 387e838895a2ba..abfecc8e791a36 100644 --- a/static/app/views/explore/replays/detail/ourlogs/openInLogsButton.tsx +++ b/static/app/views/explore/replays/detail/ourlogs/openInLogsButton.tsx @@ -4,22 +4,23 @@ import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters'; import {t} from 'sentry/locale'; import {useOrganization} from 'sentry/utils/useOrganization'; import {getLogsUrl} from 'sentry/views/explore/logs/utils'; +import {useQueryParamsSearch} from 'sentry/views/explore/queryParams/context'; type Props = { - searchTerm: string; replayId?: string; }; -export function OpenInLogsButton({searchTerm, replayId}: Props) { +export function OpenInLogsButton({replayId}: Props) { const organization = useOrganization(); const hasExploreEnabled = organization.features.includes('visibility-explore-view'); const {selection} = usePageFilters(); + const logsSearch = useQueryParamsSearch(); if (!hasExploreEnabled) { return null; } - let query = searchTerm || ''; + let query = logsSearch.formatString(); if (replayId) { const existingQuery = query ? `${query} ` : ''; query = `${existingQuery}replay_id:${replayId}`; @@ -32,7 +33,7 @@ export function OpenInLogsButton({searchTerm, replayId}: Props) { }); return ( - + {t('Open in Logs')} ); diff --git a/static/app/views/explore/replays/detail/ourlogs/ourlogFilters.spec.tsx b/static/app/views/explore/replays/detail/ourlogs/ourlogFilters.spec.tsx new file mode 100644 index 00000000000000..72f740003a60c7 --- /dev/null +++ b/static/app/views/explore/replays/detail/ourlogs/ourlogFilters.spec.tsx @@ -0,0 +1,117 @@ +import {OrganizationFixture} from 'sentry-fixture/organization'; + +import {render, screen, waitFor} from 'sentry-test/reactTestingLibrary'; + +import {PageFiltersStore} from 'sentry/components/pageFilters/store'; +import {LogsAnalyticsPageSource} from 'sentry/utils/analytics/logsAnalyticsEvent'; +import {FieldKind} from 'sentry/utils/fields'; +import {useTraceItemSearchQueryBuilderProps} from 'sentry/views/explore/components/traceItemSearchQueryBuilder'; +import {LogsQueryParamsProvider} from 'sentry/views/explore/logs/logsQueryParamsProvider'; +import {OurLogFilters} from 'sentry/views/explore/replays/detail/ourlogs/ourlogFilters'; +import {TraceItemDataset} from 'sentry/views/explore/types'; + +const baseSearchQueryBuilderProps = { + itemType: TraceItemDataset.LOGS as TraceItemDataset.LOGS, + booleanAttributes: {}, + booleanSecondaryAliases: {}, + numberAttributes: {}, + numberSecondaryAliases: {}, + stringAttributes: { + 'log.message': {key: 'log.message', name: 'log.message', kind: FieldKind.TAG}, + }, + stringSecondaryAliases: {}, + initialQuery: '', + searchSource: 'ourlogs' as const, + onSearch: jest.fn(), +}; + +function Wrapper({children}: {children: React.ReactNode}) { + return ( + + {children} + + ); +} + +function TestOurLogFilters() { + const searchQueryBuilderProviderProps = useTraceItemSearchQueryBuilderProps( + baseSearchQueryBuilderProps + ); + return ( + + ); +} + +describe('OurLogFilters', () => { + beforeEach(() => { + PageFiltersStore.init(); + PageFiltersStore.onInitializeUrlState({ + projects: [1], + environments: [], + datetime: {period: '14d', start: null, end: null, utc: false}, + }); + MockApiClient.clearMockResponses(); + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/trace-items/attributes/', + body: [], + }); + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/trace-items/attributes/validate/', + method: 'POST', + body: {attributes: {}}, + }); + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/recent-searches/', + body: [], + }); + }); + + it('renders with replay-specific placeholder text', async () => { + const organization = OrganizationFixture({features: ['visibility-explore-view']}); + + render( + + + , + {organization} + ); + + expect( + await screen.findByPlaceholderText('Search on log levels, messages, and more') + ).toBeInTheDocument(); + }); + + it('renders the Open in Logs button when explore feature is enabled', async () => { + const organization = OrganizationFixture({features: ['visibility-explore-view']}); + + render( + + + , + {organization} + ); + + expect(await screen.findByRole('button', {name: 'Open in Logs'})).toBeInTheDocument(); + }); + + it('does not render the Open in Logs button when explore feature is disabled', async () => { + const organization = OrganizationFixture({features: []}); + + render( + + + , + {organization} + ); + + await waitFor(() => + expect(screen.queryByRole('button', {name: 'Open in Logs'})).not.toBeInTheDocument() + ); + }); +}); diff --git a/static/app/views/explore/replays/detail/ourlogs/ourlogFilters.tsx b/static/app/views/explore/replays/detail/ourlogs/ourlogFilters.tsx index f74fef7ef9dcaf..a72cb61a397e44 100644 --- a/static/app/views/explore/replays/detail/ourlogs/ourlogFilters.tsx +++ b/static/app/views/explore/replays/detail/ourlogs/ourlogFilters.tsx @@ -1,58 +1,41 @@ import styled from '@emotion/styled'; -import {CompactSelect} from '@sentry/scraps/compactSelect'; -import {OverlayTrigger} from '@sentry/scraps/overlayTrigger'; - -import {SearchBar} from 'sentry/components/searchBar'; +import {SearchQueryBuilderProvider} from 'sentry/components/searchQueryBuilder/context'; import {t} from 'sentry/locale'; -import type {OurLogsResponseItem} from 'sentry/views/explore/logs/types'; +import { + TraceItemSearchQueryBuilder, + type TraceItemSearchQueryBuilderProps, + type useTraceItemSearchQueryBuilderProps, +} from 'sentry/views/explore/components/traceItemSearchQueryBuilder'; import {FiltersGrid} from 'sentry/views/explore/replays/detail/filtersGrid'; import {OpenInLogsButton} from 'sentry/views/explore/replays/detail/ourlogs/openInLogsButton'; -import {type useOurLogFilters} from 'sentry/views/explore/replays/detail/ourlogs/useOurLogFilters'; type Props = { - logItems: OurLogsResponseItem[]; + searchQueryBuilderProps: TraceItemSearchQueryBuilderProps; + searchQueryBuilderProviderProps: ReturnType; replayId?: string; -} & ReturnType; +}; + +const REPLAY_LOGS_PLACEHOLDER = t('Search on log levels, messages, and more'); export function OurLogFilters({ - logItems, replayId, - getSeverityLevels, - searchTerm, - selectValues, - setSeverityLevel, - setSearchTerm, + searchQueryBuilderProps, + searchQueryBuilderProviderProps, }: Props) { - const severityLevels = getSeverityLevels(); - return ( - - ( - - {selectValues.length === 0 ? t('Any') : triggerProps.children} - - )} - multiple - options={severityLevels} - onChange={setSeverityLevel} - size="sm" - value={selectValues.map(v => v.value)} - disabled={!severityLevels.length} - /> - - - + + + + + + ); } const StyledFiltersGrid = styled(FiltersGrid)` - grid-template-columns: max-content 1fr min-content; + grid-template-columns: 1fr min-content; `; diff --git a/static/app/views/explore/replays/detail/ourlogs/useOurLogFilters.tsx b/static/app/views/explore/replays/detail/ourlogs/useOurLogFilters.tsx deleted file mode 100644 index 6d5f73e7d2f2aa..00000000000000 --- a/static/app/views/explore/replays/detail/ourlogs/useOurLogFilters.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import {useCallback, useMemo} from 'react'; -import debounce from 'lodash/debounce'; - -import type {SelectOption} from '@sentry/scraps/compactSelect'; - -import {DEFAULT_DEBOUNCE_DURATION} from 'sentry/constants'; -import {decodeList, decodeScalar} from 'sentry/utils/queryString'; -import {useFiltersInLocationQuery} from 'sentry/utils/replays/hooks/useFiltersInLocationQuery'; -import {capitalize} from 'sentry/utils/string/capitalize'; -import type {OurLogsResponseItem} from 'sentry/views/explore/logs/types'; -import {OurLogKnownFieldKey} from 'sentry/views/explore/logs/types'; -import {getLogSeverityLevel} from 'sentry/views/explore/logs/utils'; - -type Options = { - logItems: OurLogsResponseItem[]; -}; - -type Return = { - getSeverityLevels: () => Array>; - items: OurLogsResponseItem[]; - searchTerm: string; - selectValues: Array>; - setSearchTerm: (searchTerm: string) => void; - setSeverityLevel: (val: Array>) => void; -}; - -const FILTERS = { - severity: (item: OurLogsResponseItem, severities: string[]) => { - if (severities.length === 0) { - return true; - } - - const severityText = item[OurLogKnownFieldKey.SEVERITY] as string | null; - const severityNumber = item[OurLogKnownFieldKey.SEVERITY_NUMBER] as number | null; - const level = getLogSeverityLevel(severityNumber, severityText); - - return severities.includes(level); - }, - - searchTerm: (item: OurLogsResponseItem, searchTerm: string) => { - const message = item[OurLogKnownFieldKey.MESSAGE]; - return message?.toLowerCase().includes(searchTerm.toLowerCase()) ?? false; - }, -}; - -function filterItems(options: { - filterFns: Record boolean>; - filterVals: Record; - items: T[]; -}): T[] { - const {items, filterFns, filterVals} = options; - - return items.filter(item => { - return Object.entries(filterFns).every(([key, filterFn]) => { - const filterValue = filterVals[key]; - return filterFn(item, filterValue); - }); - }); -} - -export function useOurLogFilters({logItems}: Options): Return { - const {setFilter, query} = useFiltersInLocationQuery(); - - const severityValues = useMemo( - () => decodeList(query.f_ol_severity), - [query.f_ol_severity] - ); - const searchTerm = decodeScalar(query.f_ol_search, '').toLowerCase(); - - const items = useMemo( - () => - filterItems({ - items: logItems, - filterFns: FILTERS, - filterVals: {severity: severityValues, searchTerm}, - }), - [logItems, severityValues, searchTerm] - ); - - const getSeverityLevels = useCallback(() => { - const severityLevels = Array.from( - new Set( - logItems.map(item => { - const severityText = item[OurLogKnownFieldKey.SEVERITY] as string | null; - const severityNumber = item[OurLogKnownFieldKey.SEVERITY_NUMBER] as - | number - | null; - return getLogSeverityLevel(severityNumber, severityText); - }) - ) - ).sort(); - - return severityLevels.map( - (value): SelectOption => ({ - value, - label: capitalize(value), - }) - ); - }, [logItems]); - - const debouncedSetFilter = useMemo( - () => - debounce((f_ol_search: string) => { - setFilter({f_ol_search: f_ol_search || undefined}); - }, DEFAULT_DEBOUNCE_DURATION), - [setFilter] - ); - - const setSearchTerm = useCallback( - (f_ol_search: string) => debouncedSetFilter(f_ol_search), - [debouncedSetFilter] - ); - - const setSeverityLevel = useCallback( - (value: Array>) => { - setFilter({f_ol_severity: value.map(v => v.value)}); - }, - [setFilter] - ); - - return { - getSeverityLevels, - items, - searchTerm, - selectValues: severityValues.map(s => ({value: s, label: s, qs: 'f_ol_severity'})), - setSeverityLevel, - setSearchTerm, - }; -}